package arkham.data;

import java.io.File;
import java.io.StringReader;
import java.util.*;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

import arkham.util.*;
import arkham.util.Pair.IntPair;

import arkham.data.Board.Location;
import arkham.data.Board.OtherWorld;
import arkham.data.Board.Path;
import arkham.data.Globals.*;
import arkham.data.util.AmountParser;
import arkham.data.util.EventState;
import arkham.data.util.EventUtils;
import arkham.data.util.EventState.EventStateType;
import arkham.data.util.LocationMatcher;
import arkham.jaxb.encounter.*;
import arkham.jaxb.monster.MonsterSetType;
import arkham.jaxb.resource.ResourceSetType;
import arkham.jaxb.board.BoardType;
import arkham.jaxb.ancientone.AOSetType;

/**
 * This is the unified class that contains all the game data for the game:
 * it is responsible for loading all XML files pertaining to the game,
 * maintaining game state, serializing and deserializing the game state
 * to disk on saves and loads. It also contains any flags or constants that
 * have been set.
 * @author ctheng
 *
 */
public class GameData {

    /* Decks of cards */
    private Deck<CommonItem> commonDeck;
    private Deck<UniqueItem> uniqueDeck;
    private Deck<Spell> spellDeck;
    private Deck<Skill> skillDeck;
    private Deck<Ally> allyDeck;
    private Deck<MythosCard> mythosCards;
    private Deck<GateCard> gateCards;
    private HashMap<String, InvestigatorCard> cardDatabase;
    private List<InvestigatorCard> cardArray;
    private HashMap<String, PersonalEffect> personalEffects;
    private HashMap<String, Deck<EventNode>> locationCards;
    private HashMap<String, Environment> environments;
    private HashMap<String, Rumor> rumors;
    private HashMap<String, EventNode> headlines;
    private HashMap<String, EventNode> localEvents;
    private HashMap<String, EventNode> extraEvents;
    private List<Investigator> investigatorPool;
    private List<AncientOne> aoPool;
    private List<Monster> monsterCup;
    private Monster[] monsterArray;
    private Set<String> otherWorldLocations;
    private List<GateTile> gateTiles;
    private Board mainBoard;
    private List<Board> expansionBoards;
    private int[][] distanceMatrix;
    private Map<Location, Integer> locationIndices;
    private List<LocationContainer> allLocations;
    private List<Location> arkhamLocations;
    private ResourceSet resourceSet;
    private Random rng;

    /* Flags affecting game load */
    private boolean[] loaded;
    private boolean fullyLoaded;
    private boolean gameStarted;

    /* Variables affecting game start */
    private boolean investigatorSelectionDraft;
    private int investigatorSelectionValue;

    /* Variables tracking game state */
    private int numPlayers, gameTurn;
    private int terrorLevel;
    private String[] playerNames;
    private Investigator[] investigators;
    private AncientOne ancientOne;
    private MythosCard lastMythosCard;
    private List<Pair<LocationMatcher, EventNode>> activeLocalEvents;
    private List<Environment> activeGlobalEffects;
    private MythosCard activeEnvironmentCard;
    private MythosCard activeRumorCard;
    private Rumor activeRumor;
    private GameSetupState gameSetupState;
    private GamePhase gamePhase;
    private MoveState moveState;
    private int startingPlayer, currentPlayer;

    /* Variables during movement */
    private List<Monster> combatFrame;
    private Investigator tradeRecipient;
    private String tradeDetails;

    /* Variables during encounter */
    private EventNode currentEvent;
    private String faceupEncounterNeighborhood;

    /* Variables for investigator selection */
    private List<Investigator> draftPool;
    private List<Investigator> selectionPool;

    /* Variables for monster movement during mythos */
    private List<Pair<Monster, List<Investigator>>> unassignedMonsters;
    private Map<Monster, LocationContainer> monsterDestinations;
    private List<Monster> specialMonsters;

    public GameData(int numPlayers)
    {
        loaded = new boolean[6];
        Arrays.fill(loaded, false);
        this.numPlayers = numPlayers;
        investigators = new Investigator[numPlayers];
        activeGlobalEffects = new ArrayList<Environment>();
        activeLocalEvents = new ArrayList<Pair<LocationMatcher, EventNode>>();
        cardDatabase = new HashMap<String, InvestigatorCard>();
        activeRumor = null;
        gameSetupState = GameSetupState.PRE_SETUP;
        gamePhase = GamePhase.MYTHOS_GATE;
        currentPlayer = 0;
        startingPlayer = 0;
        gameTurn = 0;
        draftPool = new ArrayList<Investigator>();
        unassignedMonsters = new ArrayList<Pair<Monster, List<Investigator>>>();
        monsterDestinations = new HashMap<Monster, LocationContainer>();
        specialMonsters = new ArrayList<Monster>();
        rng = new Random();
        combatFrame = new ArrayList<Monster>();
        faceupEncounterNeighborhood = null;
    }

    /**
     * Load all investigator cards. Files must contain XML pertaining to the
     * InvCardSet type
     * @param filenames the list of filenames to load
     */
    public void loadInvCardsFromFiles(List<File> files)
    {
        commonDeck = new Deck<CommonItem>();
        uniqueDeck = new Deck<UniqueItem>();
        spellDeck = new Deck<Spell>();
        skillDeck = new Deck<Skill>();
        allyDeck = new Deck<Ally>();
        personalEffects = new HashMap<String, PersonalEffect>();
        cardArray = new ArrayList<InvestigatorCard>();
        int count = 0;
        HashSet<String> usedNames = new HashSet<String>();
        cardDatabase.clear();
        for (File file : files)
        {
            InvCardSetType icset = loadJAXBElement(file, "arkham.jaxb.encounter");
            if (icset == null) return;
            for (int i = 0; i < icset.getCard().size(); i++)
            {
                CardType ictype = icset.getCard().get(i);
                InvestigatorCard icard = InvestigatorCard.createCardFromXml(ictype);
                if (icard == null) return;
                if (usedNames.contains(icard.getName()))
                {
                    AHLogger.error("Duplicate definition of " + icard.getName());
                    return;
                }
                else
                {
                    usedNames.add(icard.getName());
                    cardDatabase.put(icard.getName().toLowerCase(), icard);
                }
                for (int j = 0; j < ictype.getQty(); j++)
                {
                    if (j != 0)
                    {
                        icard = InvestigatorCard.createCardFromXml(ictype);
                    }
                    switch (icard.getType())
                    {
                    case COMMON:
                        commonDeck.add((CommonItem)icard);
                        break;
                    case UNIQUE:
                        uniqueDeck.add((UniqueItem)icard);
                        break;
                    case SPELL:
                        spellDeck.add((Spell)icard);
                        break;
                    case SKILL:
                        skillDeck.add((Skill)icard);
                        break;
                    case ALLY:
                        allyDeck.add((Ally)icard);
                        break;
                    }
                    icard.setId(count);
                    cardArray.add(count, icard);
                    count++;
                }
            }
            for (PersonalEffectType xmlpeff : icset.getPersonalEffect())
            {
                PersonalEffect peff = PersonalEffect.createEffectFromXml(xmlpeff);
                personalEffects.put(peff.getName(), peff);
            }
        }
        // Loaded investigator cards
        loaded[0] = true;
        AHLogger.info("GameData", count + " investigator Cards loaded...");
    }

    /**
     * Load all investigator cards. Files must contain XML pertaining to the
     * InvCardSet type
     * @param filenames the list of filenames to load
     */
    public void loadInvCardsFromText(List<String> xmltextList)
    {
        commonDeck = new Deck<CommonItem>();
        uniqueDeck = new Deck<UniqueItem>();
        spellDeck = new Deck<Spell>();
        skillDeck = new Deck<Skill>();
        allyDeck = new Deck<Ally>();
        personalEffects = new HashMap<String, PersonalEffect>();
        cardArray = new ArrayList<InvestigatorCard>();
        int count = 0;
        HashSet<String> usedNames = new HashSet<String>();
        cardDatabase.clear();
        for (String xml : xmltextList)
        {
            InvCardSetType icset = loadJAXBElement(xml, "arkham.jaxb.encounter");
            if (icset == null) return;
            for (int i = 0; i < icset.getCard().size(); i++)
            {
                CardType ictype = icset.getCard().get(i);
                InvestigatorCard icard = InvestigatorCard.createCardFromXml(ictype);
                if (icard == null) return;
                if (usedNames.contains(icard.getName()))
                {
                    AHLogger.error("Duplicate definition of " + icard.getName());
                    return;
                }
                else
                {
                    usedNames.add(icard.getName());
                    cardDatabase.put(icard.getName().toLowerCase(), icard);
                }
                for (int j = 0; j < ictype.getQty(); j++)
                {
                    if (j != 0)
                    {
                        icard = InvestigatorCard.createCardFromXml(ictype);
                    }
                    switch (icard.getType())
                    {
                    case COMMON:
                        commonDeck.add((CommonItem)icard);
                        break;
                    case UNIQUE:
                        uniqueDeck.add((UniqueItem)icard);
                        break;
                    case SPELL:
                        spellDeck.add((Spell)icard);
                        break;
                    case SKILL:
                        skillDeck.add((Skill)icard);
                        break;
                    case ALLY:
                        allyDeck.add((Ally)icard);
                        break;
                    }
                    icard.setId(count);
                    cardArray.add(count, icard);
                    count++;
                }
            }
            for (PersonalEffectType xmlpeff : icset.getPersonalEffect())
            {
                PersonalEffect peff = PersonalEffect.createEffectFromXml(xmlpeff);
                personalEffects.put(peff.getName(), peff);
            }
        }
        // Loaded investigator cards
        commonDeck.shuffle();
        uniqueDeck.shuffle();
        spellDeck.shuffle();
        skillDeck.shuffle();
        allyDeck.shuffle();
        loaded[0] = true;
        AHLogger.info("GameData", count + " investigator Cards loaded...");
    }

    /**
     * Load all investigators. Files must contain XML pertaining to the
     * InvestigatorSet type
     * @param files the list of files to load
     */
    public void loadInvestigatorsFromFiles(List<File> files)
    {
        investigatorPool = new ArrayList<Investigator>();
        for (File file : files)
        {
            InvestigatorSetType isettype = loadJAXBElement(file, "arkham.jaxb.encounter");
            if (isettype == null) return;
            for (int i = 0; i < isettype.getInvestigator().size(); i++)
            {
                Investigator inv = Investigator
                    .createInvestigatorFromXml(isettype.getInvestigator().get(i));
                if (inv == null) return;
                investigatorPool.add(inv);
            }
        }
        loaded[1] = true;
        Collections.sort(investigatorPool, new Comparator<Investigator>()
                {
                    public int compare(Investigator inv1, Investigator inv2)
                    {
                        return inv1.getName().compareTo(inv2.getName());
                    }
                });
        AHLogger.info("GameData", investigatorPool.size() + " investigators loaded...");
    }

    /**
     * Load all investigators from XML pertaining to the
     * InvestigatorSet type
     * @param xmlTextList the list of xml text to load
     */
    public void loadInvestigatorsFromText(List<String> xmlTextList)
    {
        investigatorPool = new ArrayList<Investigator>();
        for (String xml : xmlTextList)
        {
            InvestigatorSetType isettype = loadJAXBElement(xml, "arkham.jaxb.encounter");
            if (isettype == null) return;
            for (int i = 0; i < isettype.getInvestigator().size(); i++)
            {
                Investigator inv = Investigator
                    .createInvestigatorFromXml(isettype.getInvestigator().get(i));
                if (inv == null) return;
                investigatorPool.add(inv);
            }
        }
        loaded[1] = true;
        Collections.sort(investigatorPool, new Comparator<Investigator>()
                {
                    public int compare(Investigator inv1, Investigator inv2)
                    {
                        return inv1.getName().compareTo(inv2.getName());
                    }
                });
        AHLogger.info("GameData", investigatorPool.size() + " investigators loaded...");
    }

    /**
     * Load all monsters. Files must contain XML pertaining to the
     * MonsterSet type
     * @param files the list of files to load
     */
    public void loadMonstersFromFiles(List<File> files)
    {
        monsterCup = new ArrayList<Monster>();
        List<Monster> masks = new ArrayList<Monster>();
        int count = 0;
        for (File file : files)
        {
            MonsterSetType msettype = loadJAXBElement(file, "arkham.jaxb.monster");
            if (msettype == null)
            {
                return;
            }
            for (int i = 0; i < msettype.getMonster().size(); i++)
            {
                Monster monster = Monster
                    .createMonsterFromXml(msettype.getMonster().get(i));
                if (monster == null) return;
                for (int j = 0; j < msettype.getMonster().get(i).getQty(); j++)
                {
                    if (j != 0)
                    {
                        monster = Monster.createMonsterFromXml(msettype.getMonster().get(i));
                    }
                    monster.setId(count);
                    if (monster.hasAttribute(MonsterAttribute.MASK))
                    {
                        masks.add(monster);
                    }
                    else
                    {
                        monsterCup.add(monster);
                    }
                    count++;
                }
            }
        }
        loaded[2] = true;
        AHLogger.info("GameData", count + " monsters loaded...");
        monsterArray = new Monster[count];
        for (int i = 0; i < monsterCup.size(); i++)
        {
            Monster mon = monsterCup.get(i);
            monsterArray[mon.getId()] = mon;
        }
        for (int i = 0; i < masks.size(); i++)
        {
            Monster mon = masks.get(i);
            monsterArray[mon.getId()] = mon;
        }

        // shuffle
        shuffleList(monsterCup);
    }

    /**
     * Load all monsters from XML pertaining to the
     * MonsterSet type
     * @param xmlTextList the list of xml text to load
     */
    public void loadMonstersFromText(List<String> xmlTextList)
    {
        monsterCup = new ArrayList<Monster>();
        List<Monster> masks = new ArrayList<Monster>();
        int count = 0;
        for (String xml : xmlTextList)
        {
            MonsterSetType msettype = loadJAXBElement(xml, "arkham.jaxb.monster");
            if (msettype == null)
            {
                return;
            }
            for (int i = 0; i < msettype.getMonster().size(); i++)
            {
                Monster monster = Monster
                    .createMonsterFromXml(msettype.getMonster().get(i));
                if (monster == null) return;
                for (int j = 0; j < msettype.getMonster().get(i).getQty(); j++)
                {
                    if (j != 0)
                    {
                        monster = Monster
                        .createMonsterFromXml(msettype.getMonster().get(i));
                    }
                    monster.setId(count);
                    if (monster.hasAttribute(MonsterAttribute.MASK))
                    {
                        masks.add(monster);
                    }
                    else
                    {
                        monsterCup.add(monster);
                    }
                    count++;
                }
            }
        }
        loaded[2] = true;
        AHLogger.info("GameData", count + " monsters loaded...");
        monsterArray = new Monster[count];
        for (int i = 0; i < monsterCup.size(); i++)
        {
            Monster mon = monsterCup.get(i);
            monsterArray[mon.getId()] = mon;
        }
        for (int i = 0; i < masks.size(); i++)
        {
            Monster mon = masks.get(i);
            monsterArray[mon.getId()] = mon;
        }
        // shuffle
        shuffleList(monsterCup);
    }

    /**
     * Load all ancient ones. Files must contain XML pertaining to the
     * AOSet type
     * @param files the list of files to load
     */
    public void loadAncientOnesFromFiles(List<File> files)
    {
        aoPool = new ArrayList<AncientOne>();
        for (File file : files)
        {
            AOSetType aosettype = loadJAXBElement(file, "arkham.jaxb.ancientone");
            if (aosettype == null)
            {
                return;
            }
            for (int i = 0; i < aosettype.getAncientOne().size(); i++)
            {
                AncientOne ao = AncientOne
                    .createAncientOneFromXml(aosettype.getAncientOne().get(i));
                if (ao == null) return;
                aoPool.add(ao);
            }
        }
        loaded[3] = true;
        AHLogger.info("GameData", aoPool.size() + " Ancient Ones loaded...");
    }

    /**
     * Load all ancient ones from XML pertaining to the
     * AOSet type
     * @param xmlTextList the list of xml text to load
     */
    public void loadAncientOnesFromText(List<String> xmlTextList)
    {
        aoPool = new ArrayList<AncientOne>();
        for (String xml : xmlTextList)
        {
            AOSetType aosettype = loadJAXBElement(xml, "arkham.jaxb.ancientone");
            if (aosettype == null)
            {
                return;
            }
            for (int i = 0; i < aosettype.getAncientOne().size(); i++)
            {
                AncientOne ao = AncientOne
                    .createAncientOneFromXml(aosettype.getAncientOne().get(i));
                if (ao == null) return;
                aoPool.add(ao);
            }
        }
        loaded[3] = true;
        AHLogger.info("GameData", aoPool.size() + " Ancient Ones loaded...");
    }

    /**
     * Load the board. Files must contain XML pertaining to the
     * BoardType
     * @param mainBoardFile the file of the main arkham board
     * @param expansionBoardFiles files of any expansion boards
     */
    public void loadBoardsFromFiles(File mainBoardFile, List<File> expansionBoardFiles)
    {
        BoardType mbtype = loadJAXBElement(mainBoardFile, "arkham.jaxb.board");
        if (mbtype == null)
        {
            return;
        }
        allLocations = new ArrayList<LocationContainer>();
        mainBoard = Board.createBoardFromXml(mbtype);
        // TODO: remove from here if necessary
        mainBoard.init(allLocations);
        expansionBoards = new ArrayList<Board>();
        AHLogger.info("GameData", mainBoard.getName() + " loaded...");

        for (File file : expansionBoardFiles)
        {
            BoardType expbtype = loadJAXBElement(file, "arkham.jaxb.board");
            if (expbtype == null) return;
            Board expBoard = Board.createBoardFromXml(expbtype);
            if (expBoard == null) return;
            expansionBoards.add(expBoard);
            expBoard.init(allLocations);
            AHLogger.info("GameData", expBoard.getName() + " loaded...");
        }
        computeDistances();
        loaded[4] = true;
    }

    /**
     * Load the board. Files must contain XML pertaining to the
     * BoardType
     * @param mainBoardXml the Xml of the main arkham board
     * @param expansionBoardXmls Xmls of any expansion boards
     */
    public void loadBoardsFromText(String mainBoardXml, List<String> expansionBoardXmls)
    {
        BoardType mbtype = loadJAXBElement(mainBoardXml, "arkham.jaxb.board");
        if (mbtype == null)
        {
            return;
        }
        mainBoard = Board.createBoardFromXml(mbtype);
        allLocations = new ArrayList<LocationContainer>();
        // TODO: remove from here if necessary
        mainBoard.init(allLocations);
        expansionBoards = new ArrayList<Board>();
        AHLogger.info("GameData", mainBoard.getName() + " loaded...");

        for (String xml : expansionBoardXmls)
        {
            BoardType expbtype = loadJAXBElement(xml, "arkham.jaxb.board");
            if (expbtype == null) return;
            Board expBoard = Board.createBoardFromXml(expbtype);
            if (expBoard == null) return;
            expansionBoards.add(expBoard);
            expBoard.init(allLocations);
            AHLogger.info("GameData", expBoard.getName() + " loaded...");
        }
        computeDistances();
        loaded[4] = true;
    }



    /**
     * Loads the game resources such as icons, images and auxiliary data files.
     * @param files the list of xml text to load
     */
    public void loadResourcesFromFiles(List<File> files)
    {
        resourceSet = ResourceSet.getInstance();
        for (File file : files)
        {
            ResourceSetType xmlset = loadJAXBElement(file, "arkham.jaxb.resource");
            if (xmlset == null)
            {
                return;
            }
            resourceSet.addToResourceSet(xmlset);
        }

        loaded[5] = true;
    }

    /**
     * Loads the game resources such as icons, images and auxiliary data files.
     * @param xmlTextList the filenames of the resources
     */
    public void loadResourcesFromText(List<String> xmlTextList)
    {
        resourceSet = ResourceSet.getInstance();
        for (String xml : xmlTextList)
        {
            ResourceSetType xmlset = loadJAXBElement(xml, "arkham.jaxb.resource");
            if (xmlset == null)
            {
                return;
            }
            resourceSet.addToResourceSet(xmlset);
        }

        loaded[5] = true;
    }

    /**
     * Load all encounters. Files must contain XML pertaining to the
     * CardSet type
     * @param files the list of files to load
     */
    public void loadEncountersFromFiles(List<File> files)
    {
        boolean passed = true;
        for (int i = 0; i < loaded.length; i++)
        {
            passed = passed && loaded[i];
        }
        if (!passed)
        {
            AHLogger.error("Must load the rest of data types before loading encounters");
            return;
        }
        gateTiles = new ArrayList<GateTile>();
        otherWorldLocations = new HashSet<String>();
        environments = new HashMap<String, Environment> ();
        headlines = new HashMap<String, EventNode>();
        rumors = new HashMap<String, Rumor>();
        localEvents = new HashMap<String, EventNode>();
        extraEvents = new HashMap<String, EventNode>();
        mythosCards = new Deck<MythosCard>();
        gateCards = new Deck<GateCard>();
        locationCards = new HashMap<String, Deck<EventNode>>();

        int enccount = 0;
        int extracount = 0;
        /*
         * 1. Load Gate Tiles
         * 2. Load environments, rumors, and headlines
         * 3. Load Mythos cards and validate
         * 4. Load Gate cards
         * 5. Load location cards
         */
        for (File file : files)
        {
            CardSetType cstype = loadJAXBElement(file, "arkham.jaxb.encounter");
            if (cstype == null) return;

            // load gate tiles
            for (GateTileType gttype : cstype.getGatetile())
            {
                GateTile gatetile = GateTile.createGateTileFromXml(gttype);
                if (gatetile == null) return;
                otherWorldLocations.add(gatetile.getName());
                for (int i = 0; i < gttype.getQty(); i++)
                {
                    if (i != 0)
                    {
                        gatetile = GateTile.createGateTileFromXml(gttype);
                    }
                    gateTiles.add(gatetile);
                }
            }
            // load environment, rumor, headline effects. Warn if there are duplicates
            // and reject the duplicate
            for (EnvironmentType xmlenv : cstype.getEnvironment())
            {
                Environment env = Environment.createEnvironmentFromXml(xmlenv);
                if (env == null) return;
                if (environments.containsKey(env.getName()))
                {
                    AHLogger.warning("Duplicate environment " + env.getName() + " found");
                }
                else
                {
                    environments.put(env.getName(), env);
                }
            }
            for (RumorType rtype : cstype.getRumor())
            {
                Rumor rum = Rumor.createRumorFromXml(rtype);
                if (rum == null) return;
                if (rumors.containsKey(rum.getName()))
                {
                    AHLogger.warning("GameData", "Duplicate rumor " + rum.getName() + " found");
                }
                else
                {
                    rumors.put(rum.getName(), rum);
                }
            }
            for (EventNode headline : cstype.getHeadline())
            {
                if (headlines.containsKey(headline.getName()))
                {
                    AHLogger.warning("GameData", "Duplicate headline " + headline.getName() + " found");
                }
                else
                {
                    headlines.put(headline.getName(), headline);
                }
            }
            for (EventNode lev : cstype.getLocalEvent())
            {
                if (localEvents.containsKey(lev.getName()))
                {
                    AHLogger.warning("GameData", "Duplicate local event " + lev.getName() + " found");
                }
                else
                {
                    localEvents.put(lev.getName(), lev);
                }
            }
            // load mythos cards
            for (MythosCardType mtype : cstype.getMythos())
            {
                MythosCard mcard = MythosCard.createMythosFromXml(mtype);
                if (mcard == null) return;
                // check that places, etc referenced in the mythos card are present
                if (mcard.getGateLocation() != null && !isLocationValid(mcard.getGateLocation()))
                {
                    AHLogger.error("GameData", "Mythos card " + mcard.getName()
                            + " has invalid gate location.");
                    return;
                }
                for (String clueloc : mcard.getClueLocation())
                {
                    if (!isLocationValid(clueloc))
                    {
                        AHLogger.error("GameData", "Mythos card " + mcard.getName()
                                + " has invalid clue location.");
                        return;
                    }
                }
                for (Pair<String, String> effect : mcard.getEffects())
                {
                    if (effect.getA().equals("Environment"))
                    {
                        if (!environments.containsKey(effect.getB()))
                        {
                            AHLogger.error("GameData", "Mythos card " + mcard.getName()
                                    + " specified a non-existent environment: " + effect.getB());
                            return;
                        }
                    }
                    else if (effect.getA().equals("Headline"))
                    {
                        if (!headlines.containsKey(effect.getB()))
                        {
                            AHLogger.error("GameData", "Mythos card " + mcard.getName()
                                    + " specified a non-existent headline: " + effect.getB());
                            return;
                        }
                    }
                    else if (effect.getA().equals("LocalEvent"))
                    {
                        if (!localEvents.containsKey(effect.getB()))
                        {
                            AHLogger.error("GameData", "Mythos card " + mcard.getName()
                                    + " specified a non-existent local event: " + effect.getB());
                            return;
                        }
                    }
                    else if (effect.getA().equals("Rumor"))
                    {
                        if (!rumors.containsKey(effect.getB()))
                        {
                            AHLogger.error("GameData", "Mythos card " + mcard.getName()
                                    + " specified a non-existent rumor: " + effect.getB());
                            return;
                        }
                    }
                }
                mythosCards.add(mcard);
            } // end load mythos cards
            // load gate cards
            for (GateCardType gctype : cstype.getGate())
            {
                GateCard gcard = GateCard.createGateCardFromXml(gctype);
                if (gcard == null) return;
                // validate card
                for (EventNode owtype : gctype.getOtherworld())
                {
                    String owloc = owtype.getLocation();
                    if (!owloc.equals("Other") && !isOtherWorldValid(owloc))
                    {
                        AHLogger.error("GameData", "Gate card " + gcard.getName() +
                                " has invalid other world: " + owloc);
                        return;
                    }
                }
                gateCards.add(gcard);
            }

            // load location cards
            HashSet<String> dupcheck = new HashSet<String>();
            for (EventNode encounter : cstype.getEncounter())
            {
                String location = encounter.getLocation();
                if (!isLocationValid(location))
                {
                    AHLogger.warning("GameData", "Arkham encounter " + encounter.getName()
                            + " has invalid location. Ignoring...");
                    continue;
                }
                if (dupcheck.contains(encounter.getName()))
                {
                    AHLogger.warning("GameData", "Duplicate Arkham encounter found: "
                            + encounter.getName());
                }
                Deck<EventNode> deck = locationCards.get(location);
                if (deck == null)
                {
                    deck = new Deck<EventNode>();
                }
                dupcheck.add(encounter.getName());
                deck.add(encounter);
                enccount++;
                locationCards.put(location, deck);
            }

            // load extra events
            for (EventNode encounter : cstype.getExtraEvent())
            {
                if (extraEvents.containsKey(encounter.getName()))
                {
                    AHLogger.warning("GameData", "Duplicate extra encounter found: "
                            + encounter.getName());
                }
                extraEvents.put(encounter.getName(), encounter);
                extracount++;
            }

        } // end of: for filename
        fullyLoaded = true;
        AHLogger.info("GateData", mythosCards.size() + " mythos cards loaded.");
        AHLogger.info("GateData", gateCards.size() + " gate cards loaded.");
        AHLogger.info("GateData", enccount + " Arkham encounters loaded.");
        AHLogger.info("GateData", extracount + " extra encounters loaded.");

        // TODO: shuffle
        shuffleList(gateTiles);
    }

    /**
     * Load all encounters. Files must contain XML pertaining to the
     * CardSet type
     * @param xmlTextList the list of files to load
     */
    public void loadEncountersFromText(List<String> xmlTextList)
    {
        boolean passed = true;
        for (int i = 0; i < loaded.length; i++)
        {
            passed = passed && loaded[i];
        }
        if (!passed)
        {
            AHLogger.error("Must load the rest of data types before loading encounters");
            return;
        }
        gateTiles = new ArrayList<GateTile>();
        otherWorldLocations = new HashSet<String>();
        environments = new HashMap<String, Environment> ();
        headlines = new HashMap<String, EventNode>();
        rumors = new HashMap<String, Rumor>();
        localEvents = new HashMap<String, EventNode>();
        extraEvents = new HashMap<String, EventNode>();
        mythosCards = new Deck<MythosCard>();
        gateCards = new Deck<GateCard>();
        locationCards = new HashMap<String, Deck<EventNode>>();

        int enccount = 0;
        int extracount = 0;
        /*
         * 1. Load Gate Tiles
         * 2. Load environments, rumors, and headlines
         * 3. Load Mythos cards and validate
         * 4. Load Gate cards
         * 5. Load location cards
         */
        for (String xml : xmlTextList)
        {
            CardSetType cstype = loadJAXBElement(xml, "arkham.jaxb.encounter");
            if (cstype == null) return;

            // load gate tiles
            for (GateTileType gttype : cstype.getGatetile())
            {
                GateTile gatetile = GateTile.createGateTileFromXml(gttype);
                if (gatetile == null) return;
                otherWorldLocations.add(gatetile.getName());
                for (int i = 0; i < gttype.getQty(); i++)
                {
                    if (i != 0)
                    {
                        gatetile = GateTile.createGateTileFromXml(gttype);
                    }
                    gateTiles.add(gatetile);
                }
            }
            // load environment, rumor, headline effects. Warn if there are duplicates
            // and reject the duplicate
            for (EnvironmentType xmlenv : cstype.getEnvironment())
            {
                Environment env = Environment.createEnvironmentFromXml(xmlenv);
                if (env == null) return;
                if (environments.containsKey(env.getName()))
                {
                    AHLogger.warning("Duplicate environment " + env.getName() + " found");
                }
                else
                {
                    environments.put(env.getName(), env);
                }
            }
            for (RumorType rtype : cstype.getRumor())
            {
                Rumor rum = Rumor.createRumorFromXml(rtype);
                if (rum == null) return;
                if (rumors.containsKey(rum.getName()))
                {
                    AHLogger.warning("GameData", "Duplicate rumor " + rum.getName() + " found");
                }
                else
                {
                    rumors.put(rum.getName(), rum);
                }
            }
            for (EventNode headline : cstype.getHeadline())
            {
                if (headlines.containsKey(headline.getName()))
                {
                    AHLogger.warning("GameData", "Duplicate headline " + headline.getName() + " found");
                }
                else
                {
                    headlines.put(headline.getName(), headline);
                }
            }
            for (EventNode lev : cstype.getLocalEvent())
            {
                if (localEvents.containsKey(lev.getName()))
                {
                    AHLogger.warning("GameData", "Duplicate local event " + lev.getName() + " found");
                }
                else
                {
                    localEvents.put(lev.getName(), lev);
                }
            }
            // load mythos cards
            for (MythosCardType mtype : cstype.getMythos())
            {
                MythosCard mcard = MythosCard.createMythosFromXml(mtype);
                if (mcard == null) return;
                // check that places, etc referenced in the mythos card are present
                if (mcard.getGateLocation() != null && !isLocationValid(mcard.getGateLocation()))
                {
                    AHLogger.error("GameData", "Mythos card " + mcard.getName()
                            + " has invalid gate location.");
                    return;
                }
                for (String clueloc : mcard.getClueLocation())
                {
                    if (!isLocationValid(clueloc))
                    {
                        AHLogger.error("GameData", "Mythos card " + mcard.getName()
                                + " has invalid clue location.");
                        return;
                    }
                }
                for (Pair<String, String> effect : mcard.getEffects())
                {
                    if (effect.getA().equals("Environment"))
                    {
                        if (!environments.containsKey(effect.getB()))
                        {
                            AHLogger.error("GameData", "Mythos card " + mcard.getName()
                                    + " specified a non-existent environment: " + effect.getB());
                            return;
                        }
                    }
                    else if (effect.getA().equals("Headline"))
                    {
                        if (!headlines.containsKey(effect.getB()))
                        {
                            AHLogger.error("GameData", "Mythos card " + mcard.getName()
                                    + " specified a non-existent headline: " + effect.getB());
                            return;
                        }
                    }
                    else if (effect.getA().equals("LocalEvent"))
                    {
                        if (!localEvents.containsKey(effect.getB()))
                        {
                            AHLogger.error("GameData", "Mythos card " + mcard.getName()
                                    + " specified a non-existent local event: " + effect.getB());
                            return;
                        }
                    }
                    else if (effect.getA().equals("Rumor"))
                    {
                        if (!rumors.containsKey(effect.getB()))
                        {
                            AHLogger.error("GameData", "Mythos card " + mcard.getName()
                                    + " specified a non-existent rumor: " + effect.getB());
                            return;
                        }
                    }
                }
                mythosCards.add(mcard);
            } // end load mythos cards
            // load gate cards
            for (GateCardType gctype : cstype.getGate())
            {
                GateCard gcard = GateCard.createGateCardFromXml(gctype);
                if (gcard == null) return;
                // validate card
                for (EventNode owtype : gctype.getOtherworld())
                {
                    String owloc = owtype.getLocation();
                    if (!owloc.equals("Other") && !isOtherWorldValid(owloc))
                    {
                        AHLogger.error("GameData", "Gate card " + gcard.getName() +
                                " has invalid other world: " + owloc);
                        return;
                    }
                }
                gateCards.add(gcard);
            }

            // load location cards
            HashSet<String> dupcheck = new HashSet<String>();
            for (EventNode encounter : cstype.getEncounter())
            {
                String location = encounter.getLocation();
                if (!isLocationValid(location))
                {
                    AHLogger.warning("GameData", "Arkham encounter " + encounter.getName()
                            + " has invalid location. Ignoring...");
                    continue;
                }
                if (dupcheck.contains(encounter.getName()))
                {
                    AHLogger.warning("GameData", "Duplicate Arkham encounter found: "
                            + encounter.getName());
                }
                Deck<EventNode> deck = locationCards.get(location);
                if (deck == null)
                {
                    deck = new Deck<EventNode>();
                }
                dupcheck.add(encounter.getName());
                deck.add(encounter);
                enccount++;
                locationCards.put(location, deck);
            }

            // load extra events
            for (EventNode encounter : cstype.getExtraEvent())
            {
                if (extraEvents.containsKey(encounter.getName()))
                {
                    AHLogger.warning("GameData", "Duplicate extra encounter found: "
                            + encounter.getName());
                }
                extraEvents.put(encounter.getName(), encounter);
                extracount++;
            }

        } // end of: for filename
        fullyLoaded = true;
        AHLogger.info("GateData", mythosCards.size() + " mythos cards loaded.");
        AHLogger.info("GateData", gateCards.size() + " gate cards loaded.");
        AHLogger.info("GateData", enccount + " Arkham encounters loaded.");
        AHLogger.info("GateData", extracount + " extra encounters loaded.");

        // TODO: shuffle
        shuffleList(gateTiles);
        mythosCards.shuffle();
        gateCards.shuffle();
    }

    /**
     * Performs a series of validation checks. Specifically:
     * <ol>
     * <li>For investigators, checks that fixed possessions exist and that there are
     * enough to go around</li>
     * <li>Checks if start locations for investigators are properly specified</li>
     * <br>
     * Things that need to be validated in the future:
     * No item can have two "Weapon" type effects, and all items with "Weapon" defined also
     * defines the property "Hands"
     *
     * @return true if validated successfully, false otherwise
     */
    public boolean validateLoadedData()
    {
        if (!fullyLoaded)
        {
            return false;
        }
        /* Check that fixed possessions exist and there are enough to go around */
        List<String> fixedCardList = new ArrayList<String>();
        for (int i = 0; i < investigatorPool.size(); i++)
        {
            Investigator inv = investigatorPool.get(i);
            for (Pair<String, String> fixed : inv.getFixedPossessions())
            {
                if (fixed.getB().equalsIgnoreCase("Effect"))
                {
                    if (getPersonalEffect(fixed.getA()) == null)
                    {
                        AHLogger.error("Fixed possession effect " + fixed.getA() +
                                " not defined in XML data.");
                        return false;
                    }
                    continue;
                }
                if (!InvestigatorCardType.isValidCardType(fixed.getB()))
                {
                    AHLogger.error("Fixed possession has invalid type: " + fixed.getB());
                    return false;
                }
                fixedCardList.add(fixed.getA());
                InvestigatorCard card = lookupCardDatabase(fixed.getA());
                if (card == null || !card.getType().toString().equals(fixed.getB()))
                {
                    AHLogger.error(inv.getName() + " has non-existent fixed possession: " +
                            fixed.getA());
                    return false;
                }
            }
        }
        for (InvestigatorCard invcard : commonDeck)
        {
            fixedCardList.remove(invcard.getName());
        }
        for (InvestigatorCard invcard : uniqueDeck)
        {
            fixedCardList.remove(invcard.getName());
        }
        for (InvestigatorCard invcard : spellDeck)
        {
            fixedCardList.remove(invcard.getName());
        }
        for (InvestigatorCard invcard : skillDeck)
        {
            fixedCardList.remove(invcard.getName());
        }
        for (InvestigatorCard invcard : allyDeck)
        {
            fixedCardList.remove(invcard.getName());
        }
        if (fixedCardList.size() > 0)
        {
            AHLogger.error("Fixed possession " + fixedCardList.get(0) + " not defined or" +
                " has insufficient quantity.");
        }

        /* Check that start locations are valid */
        for (int i = 0; i < investigatorPool.size(); i++)
        {
            Investigator inv = investigatorPool.get(i);
            if (getLocationContainer(inv.getStartLocation()) == null)
            {
                AHLogger.error("Invalid start location for investigator " + inv.getName());
                return false;
            }
        }
        return true;
    }

    /* ------------- Information Methods --------------- */

    /**
     * Checks if the game data has been loaded
     * @return true if loaded
     */
    public boolean isLoaded()
    {
        return fullyLoaded;
    }

    /**
     * Checks if the game has been started. The game is only started after
     * characters have been chosen and all cards have been dealt out, and the
     * first mythos card drawn.
     * @return true if started
     */
    public boolean isStarted()
    {
        return gameStarted;
    }

    /**
     * Starts the game proper. The game starts right before the first mythos is
     * ready to be resolved.
     */
    public void startGame()
    {
        gameStarted = true;
    }

    /**
     * Gets the number of players
     * @return the number of players
     */
    public int getNumPlayers()
    {
        return numPlayers;
    }

    public void setPlayerNames(String[] playerArr)
    {
        if (playerArr.length == numPlayers)
        {
            playerNames = playerArr;
        }
    }

    /**
     * Gets the name of the player sitting in a certain position
     * @param index the index of his position
     * @return the player's name
     */
    public String getPlayerName(int index)
    {
        if (index < 0 || index >= numPlayers)
        {
            return null;
        }
        return playerNames[index];
    }

    /**
     * Gets the name of the player whose turn it is
     * @return the current player's name
     */
    public String getCurrentPlayerName()
    {
        if (getCurrentPlayer() >= numPlayers)
        {
            return "None";
        }
        return playerNames[getCurrentPlayer()];
    }

    /**
     * Gets the number of expansion boards used. The base set does not count.
     * @return the number of expansion boards
     */
    public int getExpansionsInPlay()
    {
        // TODO: to fill in when expansions are supported
        return expansionBoards.size();
    }

    /**
     * Gets the pool of available investigators
     * @return the pool of investigators
     */
    public List<Investigator> getInvestigatorPool()
    {
        return investigatorPool;
    }

    /**
     * Gets the pool of ancient ones
     * @return the pool of ancient ones
     */
    public List<AncientOne> getAOPool()
    {
        return aoPool;
    }

    /**
     * Gets the AO in play
     * @return the ancient one
     */
    public AncientOne getAncientOne()
    {
        return ancientOne;
    }

    /**
     * Randomly selects an ancient one and assigns it
     * @return the ancient one
     */
    public AncientOne randomizeAncientOne()
    {
        int index = rng.nextInt(aoPool.size());
        ancientOne = aoPool.get(index);
        return ancientOne;
    }

    /**
     * Sets the ancient one
     * @param ao the ancient one to set
     */
    public void setAncientOne(AncientOne ao)
    {
        if (!isStarted())
        {
            ancientOne = ao;
        }
    }

    /**
     * Gets the array of investigators in play
     * @return the array of investigators
     */
    public Investigator[] getInvestigators()
    {
        return investigators;
    }

    /**
     * Gets the investigator at a particular player position
     * @param index the index of the investigator
     * @return the investigator there, or null if none exist
     */
    public Investigator getInvestigator(int index)
    {
        if (index < 0 || index >= numPlayers)
        {
            return null;
        }
        return investigators[index];
    }

    /**
     * Gets the investigator with a particular name
     * @param name the investigator's name
     * @return the investigator with that name, or null if none exist
     */
    public Investigator getInvestigatorByName(String name)
    {
        for (int i = 0; i < investigators.length; i++)
        {
            if (investigators[i].getName().equals(name))
            {
                return investigators[i];
            }
        }
        return null;
    }

    /**
     * Gets the turn position of this investigator in the game
     * @param inv the investigator
     * @return the index of the investigator
     */
    public int getInvestigatorIndex(Investigator inv)
    {
        for (int i = 0; i < numPlayers; i++)
        {
            if (investigators[i].equals(inv))
            {
                return i;
            }
        }
        return -1;
    }

    /**
     * Assigns the investigator to a player position during game setup
     * If in draft mode, also removes the investigator from the pool.
     * @param index the index of the investigator
     * @param inv the investigator to set
     */
    public void assignInvestigator(int index, Investigator inv)
    {
        if (gameStarted)
        {
            return;
        }
        if (index >= 0 && index < numPlayers)
        {
            inv.setPlayer(getPlayerName(index));
            investigators[index] = inv;
            inv.setIndex(index);
            if (isDraftMode())
            {
                draftPool.remove(inv);
            }
        }
    }

    /**
     * Gets the resource set
     * @return the resource set
     */
    public ResourceSet getResourceSet()
    {
        return resourceSet;
    }

    /**
     * Gets the deck of common items
     * @return the common deck
     */
    public Deck<CommonItem> getCommonDeck()
    {
        return commonDeck;
    }

    /**
     * Gets the deck of unique items
     * @return the unique deck
     */
    public Deck<UniqueItem> getUniqueDeck()
    {
        return uniqueDeck;
    }

    /**
     * Gets the deck of spells
     * @return the spell deck
     */
    public Deck<Spell> getSpellDeck()
    {
        return spellDeck;
    }

    /**
     * Gets the deck of skills
     * @return the skill deck
     */
    public Deck<Skill> getSkillDeck()
    {
        return skillDeck;
    }

    /**
     * Gets the deck of allies
     * @return the ally deck
     */
    public Deck<Ally> getAllyDeck()
    {
        return allyDeck;
    }

    /**
     * Gets a deck of investigator cards
     * @param type the type of investigator card
     * @return the deck of that type
     */
    public Deck<? extends InvestigatorCard> getDeck(String type)
    {
        if (type.equals("Common"))
        {
            return commonDeck;
        }
        else if (type.equals("Unique"))
        {
            return uniqueDeck;
        }
        else if (type.equals("Spell"))
        {
            return spellDeck;
        }
        else if (type.equals("Skill"))
        {
            return skillDeck;
        }
        else if (type.equals("Ally"))
        {
            return allyDeck;
        }
        return null;
    }

    /**
     * Removes a card from its deck
     * @param card the card to remove
     */
    public void removeFromDeck(InvestigatorCard card)
    {
        switch (card.getType())
        {
        case COMMON:
            commonDeck.dealSpecificCard((CommonItem)card);
            break;
        case UNIQUE:
            uniqueDeck.dealSpecificCard((UniqueItem)card);
            break;
        case SPELL:
            spellDeck.dealSpecificCard((Spell)card);
            break;
        case SKILL:
            skillDeck.dealSpecificCard((Skill)card);
            break;
        case ALLY:
            allyDeck.dealSpecificCard((Ally)card);
            break;
        }
    }

    /**
     * Returns an investigator card to the appropriate deck.
     * @param card the card to return
     */
    public void returnToDeck(InvestigatorCard card)
    {
        switch (card.getType())
        {
        case COMMON:
            commonDeck.addToBottom((CommonItem)card);
            break;
        case UNIQUE:
            uniqueDeck.addToBottom((UniqueItem)card);
            break;
        case SPELL:
            spellDeck.addToBottom((Spell)card);
            break;
        case SKILL:
            skillDeck.addToBottom((Skill)card);
            break;
        case ALLY:
            allyDeck.addToBottom((Ally)card);
            break;
        }
    }

    /**
     * Looks up the card database for the card with the given name
     * @param name the name of the investigator card
     * @return the card with that name, or null if not found
     */
    public InvestigatorCard lookupCardDatabase(String name)
    {
        return cardDatabase.get(name.toLowerCase());
    }

    /**
     * Looks up the card array for a card with that id
     * @param id the id of the card
     * @return the card with that id, or null if not found
     */
    public InvestigatorCard lookupCardArray(int id)
    {
        if (id < 0 || id >= cardArray.size())
        {
            return null;
        }
        return cardArray.get(id);
    }

    /**
     * Gets the personal effect with the given name
     * @param name the name of the personal effect
     * @return the personal effect with that name, or null if not found
     */
    public PersonalEffect getPersonalEffect(String name)
    {
        return personalEffects.get(name);
    }

    /**
     * Gets the personal effect with the given name and creates a copy
     * @param name the name of the personal effect
     * @return a new personal effect of that name
     */
    public PersonalEffect createPersonalEffect(String name)
    {
        return personalEffects.get(name).clone();
    }

    /**
     * Looks up an effect source, if it is a personal effect or investigator
     * card
     * @param sourcestr the source string to look up
     * @return the effect source, if any
     */
    public EffectSource lookupEffectSource(String sourcestr)
    {
        if (sourcestr.startsWith("#"))
        {
            int id = Integer.parseInt(sourcestr.substring(1));
            return lookupCardArray(id);
        }
        else if (sourcestr.startsWith("%"))
        {
            String name = sourcestr.substring(1);
            return getPersonalEffect(name);
        }
        else return null;
    }

    /**
     * Gets the deck of mythos cards
     * @return the mythos card deck
     */
    public Deck<MythosCard> getMythosDeck()
    {
        return mythosCards;
    }

    /**
     * Gets the deck of gate cards
     * @return the gate card deck
     */
    public Deck<GateCard> getGateDeck()
    {
        return gateCards;
    }

    /**
     * Gets the terror level
     * @return the terror level
     */
    public int getTerrorLevel()
    {
        return terrorLevel;
    }

    /**
     * Raises the terror level by a specified amount
     * @param levels the amount to raise by
     */
    public void raiseTerrorLevel(int levels)
    {
        terrorLevel++;
    }

    /**
     * Gets the last drawn mythos card
     * @return the last drawn mythos
     */
    public MythosCard getLastDrawnMythos()
    {
        return lastMythosCard;
    }

    /**
     * Gets a headline by name.
     * @param name the name of the headline
     * @return the headline with that name, or null if not found
     */
    public EventNode getHeadline(String name)
    {
        return headlines.get(name);
    }

    /**
     * Gets a local event by name. Local events are events that trigger when
     * investigators end their movement on a specific location.
     * @param name the name of the local event
     * @return the local event with that name, or null if not found
     */
    public EventNode getLocalEvent(String name)
    {
        return localEvents.get(name);
    }

    /**
     * Gets a rumor by name.
     * @param name the name of the rumor
     * @return the rumor with that name, or null if not found
     */
    public Rumor getRumor(String name)
    {
        return rumors.get(name);
    }

    /**
     * Gets an environment by name
     * @param name the name of the environment
     * @return the environment with that name, or null if not found
     */
    public Environment getEnvironment(String name)
    {
        return environments.get(name);
    }

    /**
     * @return the activeEnvironmentCard
     */
    public MythosCard getActiveEnvironmentCard()
    {
        return activeEnvironmentCard;
    }

    /**
     * @param activeRumorCard the activeRumorCard to set
     */
    public void setActiveRumorCard(MythosCard activeRumorCard)
    {
        this.activeRumorCard = activeRumorCard;
    }

    /**
     * @return the activeRumorCard
     */
    public MythosCard getActiveRumorCard()
    {
        return activeRumorCard;
    }

    /**
     * Gets a list of active environments
     * @return the list of active environments
     */
    public List<Environment> getActiveEnvironments()
    {
        return activeGlobalEffects;
    }

    /**
     * Clears all environmental effects from old environments, and
     * refreshes the environmental mythos card
     */
    public void renewEnvironment()
    {
        if (activeEnvironmentCard != null)
        {
            for (Pair<String, String> pair : activeEnvironmentCard.getEffects())
            {
                if (pair.getA().equals("LocalEvent"))
                {
                    removeLocalEvent(pair.getB());
                }
                else if (pair.getA().equals("Environment"))
                {
                    removeByName(activeGlobalEffects, pair.getB());
                }
            }
        }
        activeEnvironmentCard = lastMythosCard;
    }

    /**
     * Search for investigator global effects that affect this investigator
     * @param inv the investigator to search
     * @return a list of global effects that affect this investigator
     */
    public List<EffectType> searchInvestigatorGlobalEffects(Investigator inv)
    {
        List<EffectType> list = new ArrayList<EffectType>();
        for (Environment env : activeGlobalEffects)
        {
            if (env.getTarget().equals("Investigator:All"))
            {
                list.addAll(env.getEnvironmentEffects());
            }
            if (inv.getLocation().isArkhamLocation() && env.getTarget().equals("Investigator:Arkham"))
            {
                list.addAll(env.getEnvironmentEffects());
            }
        }
        return list;
    }

    /**
     * Search for monster enhancement global effects that affect this monster
     * @param name the name of the monster to search, or "Any" to search for effects affecting
     * all monsters
     * @return a list of global effects that affect this investigator
     */
    public List<EffectType> searchMonsterGlobalEffects(Monster mon)
    {
        List<EffectType> list = new ArrayList<EffectType>();
        for (Environment env : activeGlobalEffects)
        {
            if (env.getType().equals("Monster Enhancement"))
            {
                boolean applies = false;
                if (env.getTarget().equals(mon.getName()) || env.getTarget().equals("Monster:All"))
                {
                    applies = true;
                }
                if (env.getTarget().startsWith("Attribute:"))
                {
                    String attribStr = env.getTarget().substring(10);
                    MonsterAttribute attrib = MonsterAttribute.getAttribute(attribStr);
                    if (attrib != null && mon.hasAttribute(attrib))
                    {
                        applies = true;
                    }
                }
                if (applies)
                {
                    list.addAll(env.getEnvironmentEffects());
                }
            }
        }
        return list;
    }

    /**
     * Gets a monster's modified stat, based on the environmental effects
     * @param mon the monster
     * @param stat the stat to search for
     * @return the modified stat, if any
     */
    public int getModifiedStat(Monster mon, int stat)
    {
        List<EffectType> list = searchMonsterGlobalEffects(mon);
        int basevalue = 0, mods = 0;
        switch (stat)
        {
        case Monster.STAT_COMBAT_RATING:
            filterEffectsByDetails(list, "Combat Rating");
            basevalue = mon.getCombatRating();
            if (basevalue == Monster.NO_COMBAT_CHECK_RATING)
            {
                return basevalue;
            }
            break;
        case Monster.STAT_HORROR_RATING:
            filterEffectsByDetails(list, "Horror Rating");
            basevalue = mon.getHorrorRating();
            if (basevalue == Monster.NO_HORROR_CHECK_RATING)
            {
                return basevalue;
            }
            break;
        case Monster.STAT_COMBAT_DAMAGE:
            filterEffectsByDetails(list, "Combat Damage");
            if (mon.getCombatRating() == Monster.NO_COMBAT_CHECK_RATING)
            {
                return 0;
            }
            basevalue = mon.getCombatDamage();
            break;
        case Monster.STAT_HORROR_DAMAGE:
            filterEffectsByDetails(list, "Horror Damage");
            if (mon.getHorrorRating() == Monster.NO_HORROR_CHECK_RATING)
            {
                return 0;
            }
            basevalue = mon.getHorrorDamage();
            break;
        case Monster.STAT_AWARENESS:
            filterEffectsByDetails(list, "Awareness");
            basevalue = mon.getAwareness();
            break;
        case Monster.STAT_TOUGHNESS:
            filterEffectsByDetails(list, "Toughness");
            basevalue = mon.getToughness();
            break;
        }
        for (EffectType effect : list)
        {
            if (effect.getType().equals("Change Stat"))
            {
                basevalue = Integer.parseInt(effect.getAmount());
            }
            else if (effect.getType().equals("Add Stats"))
            {
                mods += Integer.parseInt(effect.getAmount());
            }
        }
        return basevalue + mods;
    }

    /**
     * Search for gate global effects that affect particular gate
     * @param gate the gate to check
     * @return the list of global effects that affect that gate
     */
    public List<EffectType> searchGateGlobalEffects(GateTile gate)
    {
        List<EffectType> list = new ArrayList<EffectType>();
        for (Environment env : activeGlobalEffects)
        {
            if (env.getType().equals("Gate Effect"))
            {
                boolean applies = false;
                if (env.getTarget().equals(gate.getName()) || env.getTarget().equals("Gate:All"))
                {
                    list.addAll(env.getEnvironmentEffects());
                }
            }
        }
        return list;
    }

    /**
     * Adds a local event which is NOT the result of the active environment
     * @param en the local event
     */
    public void addLocalEvent(EventNode en)
    {
        LocationMatcher lm = new LocationMatcher(en.getLocation());
        activeLocalEvents.add(new Pair<LocationMatcher, EventNode>(lm, en));
    }

    /**
     * Removes a local event which is NOT the result of the active environment
     * @param eventName the name of the event
     */
    public void removeLocalEvent(String eventName)
    {
        Iterator<Pair<LocationMatcher, EventNode>> itr = activeLocalEvents.iterator();
        for (; itr.hasNext(); itr.next())
        {
            Pair<LocationMatcher, EventNode> pair = itr.next();
            if (pair.getB().getName().equals(eventName))
            {
                itr.remove();
            }
        }
    }

    /**
     * Gets the local events that are active at a location
     * @param loc the location container to check
     * @return a list of events that would trigger
     */
    public List<EventNode> getLocalEvents(LocationContainer loc)
    {
        List<EventNode> list = new ArrayList<EventNode>();
        for (Pair<LocationMatcher, EventNode> pair : activeLocalEvents)
        {
            if (pair.getA().matches(loc))
            {
                list.add(pair.getB());
            }
        }
        return list;
    }

    /**
     * Returns the active rumor if any
     * @return the active rumor, or none if there isn't one
     */
    public Rumor getActiveRumor()
    {
        return activeRumor;
    }

    /**
     * Sets the active rumor
     * @param r the rumor to set
     */
    public void setActiveRumor(Rumor r)
    {
        activeRumor = r;
    }

    /**
     * Gets the main board
     * @return the main board
     */
    public Board getMainBoard()
    {
        return mainBoard;
    }

    /**
     * Gets the list of expansion boards
     * @return the list of expansion boards
     */
    public List<Board> getExpansionBoards()
    {
        return expansionBoards;
    }

    /**
     * Gets the list of gate tiles
     * @return the list of gate tiles
     */
    public List<GateTile> getGateTiles()
    {
        return gateTiles;
    }

    /**
     * Gets the monster cup
     * @return the monster cup
     */
    public List<Monster> getMonsters()
    {
        return monsterCup;
    }

    /**
     * Gets but does not remove a monster from the cup by its id
     * @param id the id of the monster
     * @return the monster
     */
    public Monster getMonsterById(int id)
    {
        if (id < 0 || id >= monsterArray.length)
        {
            return null;
        }
        return monsterArray[id];
    }

    /**
     * Gets but does not remove the first monster in the monster cup with that name
     * @param name the name of the monster
     * @return the first monster with that name in the monster cup, or null
     * if there are none
     */
    public Monster getMonsterByName(String name)
    {
        Monster mon = retrieveByName(monsterCup, name);
        return mon;
    }

    /**
     * Draws out a monster from the cup by id
     * @param id the id of the monster
     * @return the monster from the cup, if any
     */
    public Monster drawMonsterById(int id)
    {
        Monster mon = removeById(monsterCup, id);
        return mon;
    }

    /**
     * Gets the list of unassigned monsters
     * @return the unassignedMonsters
     */
    public List<Pair<Monster, List<Investigator>>> getUnassignedMonsters()
    {
        return unassignedMonsters;
    }

    /**
     * Gets the map of monster destinations
     * @return the monsterDestinations
     */
    public Map<Monster, LocationContainer> getMonsterDestinations()
    {
        return monsterDestinations;
    }

    /**
     * Gets the list of monsters with special movement that have yet to be
     * resolved
     * @return the list of special movement monsters
     */
    public List<Monster> getSpecialMonsters()
    {
        return specialMonsters;
    }

    /**
     * Checks if the board is at the monster limit, which is 3
     * more than the number of players
     * @return true if the board is at the monster limit
     */
    public boolean atMonsterLimit()
    {
        return mainBoard.getNumMonsters() + mainBoard.getNumMonstersInSky() >= getMonsterLimit();
    }

    /**
     * Gets the monster limit
     * @return the monster limit
     */
    public int getMonsterLimit()
    {
        return numPlayers + 3;
    }

    /**
     * Gets the outskirts limit, which is 8 - number of players
     * @return the outskirts limit
     */
    public int getOutskirtsLimit()
    {
        return 8 - numPlayers;
    }

    /**
     * Counts the total number of open gates
     * @return the number of open gates
     */
    public int getNumOpenGates()
    {
        int sum = mainBoard.getNumOpenGates();
        for (Board b : expansionBoards)
        {
            sum += b.getNumOpenGates();
        }
        return sum;
    }

    /**
     * Gets a list of all locations with open gates
     * @return a list of all open gate locations
     */
    public List<Location> getOpenGateLocations()
    {
        List<Location> list = new ArrayList<Location>();
        list.addAll(mainBoard.getAllOpenGates());
        for (Board b : expansionBoards)
        {
            list.addAll(b.getAllOpenGates());
        }
        return list;
    }

    /**
     * gets the investigator selection mode
     * @return true for draft mode, false for single pick
     */
    public boolean isDraftMode()
    {
        return investigatorSelectionDraft;
    }

    /**
     * Sets the investigator selection mode
     * @param draftMode set to true for draft mode, false for single pick
     */
    public void setDraftMode(boolean draftMode)
    {
        this.investigatorSelectionDraft = draftMode;
    }

    /**
     * Gets the size of the draft pool, or the number of investigator choices
     * per player.
     * @return the draftPoolValue
     */
    public int getInvestigatorSelectionValue()
    {
        return investigatorSelectionValue;
    }

    /**
     * Sets the size of the draft pool, or the number of investigator choices
     * per player.
     * @param invSelectValue the draftPoolValue to set
     */
    public void setInvestigatorSelectionValue(int invSelectValue)
    {
        this.investigatorSelectionValue = invSelectValue;
    }

    /**
     * Gets the game setup state
     * @return the game setup state
     */
    public GameSetupState getGameSetupState()
    {
        return gameSetupState;
    }

    /**
     * Sets the game setup state
     * @param gss the state to set
     */
    public void setGameSetupState(GameSetupState gss)
    {
        this.gameSetupState = gss;
    }

    /**
     * Gets the game phase
     * @return the game phase
     */
    public GamePhase getGamePhase()
    {
        return gamePhase;
    }

    /**
     * Sets the game phase
     * @param phase the game phase to set
     */
    public void setGamePhase(GamePhase phase)
    {
        this.gamePhase = phase;
    }

    /**
     * Sets the move state
     * @param moveState the moveState to set
     */
    public void setMoveState(MoveState moveState)
    {
        this.moveState = moveState;
    }

    /**
     * Gets the move state
     * @return the moveState
     */
    public MoveState getMoveState()
    {
        return moveState;
    }

    /**
     * Gets the current player's index
     * @return the current player's index
     */
    public int getCurrentPlayer()
    {
        return currentPlayer;
    }

    /**
     * Gets the current investigator
     * @return the current investigator
     */
    public Investigator getCurrentInvestigator()
    {
        return getInvestigator(currentPlayer);
    }

    /**
     * Advances the turn order
     * @return true if all players have completed, false otherwise
     */
    public boolean advanceTurnOrder()
    {
        currentPlayer++;
        if (currentPlayer >= numPlayers)
        {
            currentPlayer -= numPlayers;
        }
        return (currentPlayer == startingPlayer);
    }

    /**
     * Resets the turn order
     * @param turnOrder the current player's id whose turn it is
     */
    public void resetTurnOrder(int turnOrder)
    {
        currentPlayer = turnOrder;
    }

    /**
     * Gets the index of the starting player
     * @return the startingPlayer
     */
    public int getStartingPlayer() {
        return startingPlayer;
    }

    /**
     * Advances the starting player to the next player
     */
    public void advanceStartingPlayer()
    {
        startingPlayer++;
        if (startingPlayer >= numPlayers)
        {
            startingPlayer -= numPlayers;
        }
    }

    /**
     * Advances the game turn
     * @return the game turn
     */
    public int advanceGameTurn() {
        gameTurn++;
        return gameTurn;
    }

    /**
     * @return the gameTurn
     */
    public int getGameTurn() {
        return gameTurn;
    }

    /**
     * Gets the pool of investigators that will be drafted
     * @return the pool of investigators
     */
    public List<Investigator> getInvestigatorDraftPool()
    {
        return draftPool;
    }

    /**
     * Gets the pool of investigators that can be selected from
     * @return the pool of investigators
     */
    public List<Investigator> getInvestigatorSelectionPool()
    {
        return selectionPool;
    }

    /* -------------- Action Methods ---------------
     * Note: All in game actions should be carried out
     * using the methods in game data. This is to allow
     * synchronization of the different data structures.
     */

    /**
     * Drafts investigators based on draft mode and selection number.
     * If draft mode is on, the selection value of investigators will be
     * randomly drawn and placed into the draft pool. If draft mode is off
     * (choice mode), the selection value of investigators will be drafted
     * per player and placed into the draft pool.
     * @return the draft pool
     */
    public List<Investigator> draftInvestigators()
    {
        shuffleList(investigatorPool);
        if (isDraftMode())
        {
            for (int i = 0; i < investigatorSelectionValue; i++)
            {
                draftPool.add(investigatorPool.get(i));
            }
            Collections.sort(draftPool, NamedEntity.NameComparator);
        }
        else
        {
            for (int i = 0; i < investigatorSelectionValue * numPlayers; i++)
            {
                draftPool.add(investigatorPool.get(i));
            }
        }
        return draftPool;
    }

    /**
     * Initializes the investigator by giving him his fixed possessions and
     * placing him at his start location
     * @param inv the investigator to initialize
     */
    public void initializeInvestigator(Investigator inv)
    {
        inv.initialize();
        LocationContainer con = getLocationContainer(inv.getStartLocation());
        moveInvestigatorTo(inv, con);
        if (inv.getName().equals("Kate Winthrop") && con.getName().equals("Science Building"))
        {
            ((Location)con).setClues(0);
        }
        // give fixed possessions
        for (Pair<String, String> pair : inv.getFixedPossessions())
        {
            if (pair.getB().equals("Common"))
            {
                inv.gainInvestigatorCard(Deck.dealSpecificCard(commonDeck, pair.getA()));
            }
            else if (pair.getB().equals("Unique"))
            {
                inv.gainInvestigatorCard(Deck.dealSpecificCard(uniqueDeck, pair.getA()));
            }
            else if (pair.getB().equals("Spell"))
            {
                inv.gainInvestigatorCard(Deck.dealSpecificCard(spellDeck, pair.getA()));
            }
            else if (pair.getB().equals("Skill"))
            {
                inv.gainInvestigatorCard(Deck.dealSpecificCard(skillDeck, pair.getA()));
            }
            else if (pair.getB().equals("Ally"))
            {
                inv.gainInvestigatorCard(Deck.dealSpecificCard(allyDeck, pair.getA()));
            }
            else if (pair.getB().equals("Effect"))
            {
                inv.applyStatus(getPersonalEffect(pair.getA()));
            }
        }
    }

    /**
     * Initializes the investigator without items but with
     * placing him at his start location
     * @param inv the investigator to initialize
     */
    public void initializeInvestigatorWithoutItems(Investigator inv)
    {
        inv.initialize();
        LocationContainer con = getLocationContainer(inv.getStartLocation());
        moveInvestigatorTo(inv, con);
        if (inv.getName().equals("Kate Winthrop") && con.getName().equals("Science Building"))
        {
            ((Location)con).setClues(0);
        }
        // give fixed effects only
        for (Pair<String, String> pair : inv.getFixedPossessions())
        {
            if (pair.getB().equals("Effect"))
            {
                inv.applyStatus(getPersonalEffect(pair.getA()));
            }
        }
    }

    /**
     * Draws a random mythos card
     * @return the mythos card drawn
     */
    public MythosCard drawMythosCard()
    {
        while (true)
        {
            if (lastMythosCard != null)
            {
                mythosCards.addToBottom(lastMythosCard);
            }
            lastMythosCard = mythosCards.deal();
            if (!lastMythosCard.getType().equals(MythosType.RUMOR) || activeRumor == null)
            {
                /* If there is an existing rumor, redraw new ones */
                break;
            }
        }
        lastMythosCard = Deck.dealSpecificCard(mythosCards, "Sunny and Clear");
        return lastMythosCard;
    }

    /**
     * Draws out the mythos card with the specific name, and sets last drawn
     * mythos card.
     * @param name the name of the card
     * @return the mythos card drawn
     */
    public MythosCard drawMythosCard(String name)
    {
        if (lastMythosCard != null)
        {
            mythosCards.addToBottom(lastMythosCard);
        }
        lastMythosCard = Deck.dealSpecificCard(getMythosDeck(), name);
        return lastMythosCard;
    }

    /**
     * Sets up the game by carrying out start of game actions.
     * <ul>
     * <li>Setting up the board</li>
     * </ul>
     */
    public void setupGame()
    {
        mainBoard.setupBoard();
        for (Board expBoard : expansionBoards)
        {
            expBoard.setupBoard();
        }
    }

    /**
     * Moves a monster to a new location
     * @param mon the monster to move
     * @param loc the location to move to. Specify null to move the monster
     * off the board
     */
    public void moveMonsterTo(Monster mon, LocationContainer loc)
    {
        if (mon == null) return;
        // check if the monster is in a location
        if (mon.getLocationName() != null && isMonsterLocationValid(mon.getLocationName()))
        {
            // TODO: for exp boards
            LocationContainer oldLoc = getLocationContainer(mon.getLocationName());
            removeByName(oldLoc.getMonsterList(), mon.getName());
        }
        if (loc != null)
        {
            loc.getMonsterList().add(mon);
            mon.setLocation(loc);
        }
        else
        {
            mon.setLocation(null);
        }
    }

    /**
     * Moves an investigator to a new location
     * @param inv the investigator to move
     * @param loc the location to move to
     */
    public void moveInvestigatorTo(Investigator inv, LocationContainer loc)
    {
        if (inv == null || loc == null)
        {
            return;
        }
        // check if the investigator is in a location
        if (inv.getLocationName() != null && isInvestigatorLocationValid(inv.getLocationName()))
        {
            removeByName(inv.getLocation().getInvestigatorList(), inv.getName());
        }
        loc.getInvestigatorList().add(inv);
        inv.setLocation(loc);
    }

    /**
     * Opens a new gate on a location.
     * This performs the following actions:
     * <ul>
     * <li>Checks if the location is sealed, if so, do nothing.
     * <li>Checks if the location already has a gate, if so, perform
     * a monster surge.
     * <li>If neither of the above, draws the top gate tile and places it
     * on the location, then depending on the number of players, spawns
     * 1 or 2 monsters from the monster cup on the location
     * <li>If an investigator is on the location, sucks him in and delays
     * him in the other world.
     * </ul>
     * @param loc
     */
    public int openNewGateOn(Location loc)
    {
        if (loc.isSealed())
        {
            return Globals.GATE_SEALED;
        }
        if (loc.hasOpenGate())
        {
            return Globals.MONSTER_SURGE;
        }
        else
        {
            GateTile tile = gateTiles.remove(0);
            loc.openNewGate(tile);
            return Globals.GATE_OPENED;
        }
    }

    /**
     * Opens a new gate on a location
     * @param loc the location to open on
     * @return the gate opened
     */
    public GateTile openRandomGateOn(Location loc)
    {
        if (loc.isSealed() || loc.hasOpenGate())
        {
            return null;
        }
        GateTile tile = gateTiles.remove(0);
        loc.openNewGate(tile);
        return tile;
    }

    /**
     * Opens a specific gate at a location. This method is for the client's
     * use.
     * @param gate the gate to open
     * @param location the location to open on
     */
    public void openGateOnLocation(String gate, String location)
    {
        Location loc = getLocation(location);
        GateTile gateTile = retrieveByName(gateTiles, gate);
        loc.openNewGate(gateTile);
    }

    /**
     * Draws out a random monster from the cup.
     * @return the monster drawn
     */
    public Monster drawRandomMonster()
    {
        int index = rng.nextInt(monsterCup.size());
        Monster mon = monsterCup.remove(index);
        //Monster mon = removeByName(monsterCup, "Vampire");
        return mon;
    }

    /**
     * Gets but does not remove a random monster from the cup. This
     * is for monsters appearing in encounters
     * @return the monster drawn
     */
    public Monster getRandomMonster()
    {
        int index = rng.nextInt(monsterCup.size());
        Monster mon = monsterCup.get(index);
        return mon;
    }

    /**
     * Spawns a monster at a location. If the board is at its limit, moves
     * it to outskirts instead. If the outskirts is also at the limit,
     * clears the outskirts. Does not adjust terror level. This method is
     * for the client's use.
     * @param monId the string form of the monster's id
     * @param location the location to spawn on
     */
    public void spawnMonsterOnLocation(String monId, String location)
    {
        int id = Integer.parseInt(monId);
        Monster mon = removeById(monsterCup, id);
        Location loc = getLocation(location);
        if (atMonsterLimit())
        {
            LocationContainer outskirts = getLocationContainer(Board.OUTSKIRTS_NAME);
            outskirts.getMonsterList().add(mon);
            if (outskirts.getNumMonsters() > getOutskirtsLimit())
            {
                monsterCup.addAll(outskirts.getMonsterList());
                outskirts.getMonsterList().clear();
            }
        }
        else
        {
            moveMonsterTo(mon, loc);
        }
    }

    /**
     * Cycles through all locations and returns all monsters on locations
     * matching the specified description back to the monster cup
     * @param details the locations to return monsters from
     */
    public void returnMonstersOn(String details)
    {
        LocationMatcher lm = new LocationMatcher(details);
        for (LocationContainer lc : allLocations)
        {
            if (lm.matches(lc))
            {
                monsterCup.addAll(lc.getMonsterList());
                lc.getMonsterList().clear();
            }
        }
    }

    /**
     * Returns all monsters with specified names on the board back to the
     * monster cup.
     * @param names the names of monsters to return, in CSV form
     * @return true if any monsters were returned
     */
    public boolean returnMonstersNamed(String names)
    {
        String[] tokens = names.split(",");
        List<Monster> list = new ArrayList<Monster>();
        for (int i = 0; i < tokens.length; i++)
        {
            for (LocationContainer loc : allLocations)
            {
                removeAllByName(loc.getMonsterList(), tokens[i], list);
            }
        }
        monsterCup.addAll(list);
        return !list.isEmpty();
    }

    /**
     * Moves all monsters to their locations after monster movement. This method
     * is for the client's use.
     * @param cmdString the string passed by the server, which is in the form of
     * a CSV string: &lt;mon id&gt;, &lt;location name&gt;, ...
     */
    public void moveMonsters(String cmdString)
    {
        if (cmdString.trim().equals(""))
        {
            return;
        }
        String[] tokens = cmdString.split(",");
        for (int i = 0; i < tokens.length; i+=2)
        {
            Monster mon = getMonsterById(Integer.parseInt(tokens[i]));
            Location loc = getLocation(tokens[i + 1]);
            moveMonsterTo(mon, loc);
        }
    }

    /**
     * Prepares two lists for monster movement: a list containing
     * the destinations of monsters who destination is definite, and
     * a list of monsters whose destination have yet to be determined.
     */
    public void prepareMonsterMovement()
    {
        unassignedMonsters.clear();
        monsterDestinations.clear();
        specialMonsters.clear();
        MythosMovement movement = lastMythosCard.getMovement();
        for (Map.Entry<Location, Integer> locEntry : locationIndices.entrySet())
        {
            Location loc = locEntry.getKey();
            for (Monster mon : loc.getMonsterList())
            {
                int moveType = movement.getMovement(mon);
                if (moveType == Globals.DOES_NOT_MOVE ||
                        loc.getNumInvestigators() > 0 ||
                        mon.getMovement().equals(MonsterMovement.STATIONARY))
                {
                    /*
                     * Do not move if the symbol does not match, or
                     * there is an investigator present, or the monster is
                     * stationary.
                     */
                    continue;
                }
                LocationContainer dest = null;
                switch (mon.getMovement())
                {
                case NORMAL:
                    if (moveType == Globals.MOVES_ON_WHITE)
                    {
                        dest = loc.getWhiteEdge();
                    }
                    else if (moveType == Globals.MOVES_ON_BLACK)
                    {
                        dest = loc.getBlackEdge();
                    }
                    break;
                case FAST:
                    if (moveType == Globals.MOVES_ON_WHITE)
                    {
                        Location temp = loc.getWhiteEdge();
                        if (temp.getWhiteEdge() != null
                                && temp.getNumInvestigators() == 0)
                        {
                            dest = temp.getWhiteEdge();
                        }
                        else
                        {
                            dest = temp;
                        }
                    }
                    else
                    {
                        Location temp = loc.getBlackEdge();
                        if (temp.getBlackEdge() != null
                                && temp.getNumInvestigators() == 0)
                        {
                            dest = temp.getBlackEdge();
                        }
                        else
                        {
                            dest = temp;
                        }
                    }
                    break;
                case SPECIAL:
                    int die = mon.getSpecialMovementDie();
                    String type = mon.getSpecialMovementEffect();
                    if (type.equals("Nearest Investigator"))
                    {
                        int res = rollOneDie();
                        AHLogger.info("A die was rolled for " + mon.getName() + " movement: " + res);
                        if (res < die)
                        {
                            continue;
                        }
                        /* calculate nearest investigator */
                        int locindex = locEntry.getValue();
                        List<Location> bestLocations = new ArrayList<Location>();
                        int best = 99999;
                        for (Map.Entry<Location, Integer> entry : locationIndices.entrySet())
                        {
                            if (entry.getKey().getNumInvestigators() == 0 ||
                                    entry.getKey().isStreet() ||
                                    entry.getKey().getName().equals("St. Mary's Hospital") ||
                                    entry.getKey().getName().equals("Arkham Asylum"))
                            {
                                /* skip if it contains no investigators or location is
                                 * asylum or hospital */
                                continue;
                            }
                            int dist = distanceMatrix[locindex][entry.getValue()];
                            if (dist < best)
                            {
                                best = dist;
                                bestLocations.clear();
                                bestLocations.add(entry.getKey());
                            }
                            else if (dist == best)
                            {
                                bestLocations.add(entry.getKey());
                            }
                        }
                        /*
                         * Check how many possible locations are there. If there are
                         * more than one, check the sneak value of the investigators.
                         */
                        if (bestLocations.size() == 0)
                        {
                            /* If there are no investigators in arkham locations, then
                             * the monster doesn't move.
                             */
                            continue;
                        }
                        else if (bestLocations.size() == 1)
                        {
                            dest = bestLocations.get(0);
                        }
                        else
                        {
                            List<Investigator> lsinv = getLowestSneakInvestigators(bestLocations);
                            if (lsinv.size() == 1)
                            {
                                dest = lsinv.get(0).getLocation();
                            }
                            else
                            {
                                unassignedMonsters.add(new Pair<Monster, List<Investigator>>
                                    (mon, lsinv));
                            }
                        }
                    } // end of "nearest investigator"
                    else
                    {
                        /* monsters with special movement that do not move */
                        specialMonsters.add(mon);
                    }
                    break;
                case FLYING:
                    /* monsters in this loop will be on the board. Search all squares
                     * one space away, otherwise go to the sky */
                    List<Location> streetList = new ArrayList<Location>();
                    for (Location adjLoc : loc.getEdges())
                    {
                        if (adjLoc.isStreet())
                        {
                            streetList.add(adjLoc);
                        }
                    }
                    List<Investigator> lsinv = getLowestSneakInvestigators(streetList);
                    if (lsinv.size() == 0)
                    {
                        dest = getLocationContainer(Board.SKY_NAME);
                    }
                    else if (lsinv.size() == 1)
                    {
                        dest = lsinv.get(0).getLocation();
                    }
                    else
                    {
                        dest = null;
                        unassignedMonsters.add(new Pair<Monster, List<Investigator>>
                                (mon, lsinv));
                    }
                    break;
                case AQUATIC:
                    //TODO: to be implemented
                    break;
                case STALKER:
                    //TODO: to be implemented
                    break;
                }
                if (dest != null)
                {
                    monsterDestinations.put(mon, dest);
                }
            } // end of for monster
        } // end of for location

        /* Handle flying monsters in the sky */
        LocationContainer sky = getLocationContainer(Board.SKY_NAME);
        for (Monster mon : sky.getMonsterList())
        {
            int moveType = movement.getMovement(mon);
            if (moveType == Globals.DOES_NOT_MOVE)
            {
                continue;
            }
            List<Location> streetList = new ArrayList<Location>();
            for (Map.Entry<Location, Integer> entry : locationIndices.entrySet())
            {
                if (entry.getKey().isStreet())
                {
                    streetList.add(entry.getKey());
                }
            }
            List<Investigator> lsinv = getLowestSneakInvestigators(streetList);
            if (lsinv.size() == 1)
            {
                monsterDestinations.put(mon, lsinv.get(0).getLocation());
            }
            else
            {
                unassignedMonsters.add(new Pair<Monster, List<Investigator>>
                    (mon, lsinv));
            }
        }
    }

    /**
     * Searches among the list of locations for the investigator(s) with the
     * lowest sneak in different locations. Removes investigators in duplicate
     * locations.
     * @param locList the list of locations
     * @return the list of investigators with the lowest sneak in these locations
     */
    private List<Investigator> getLowestSneakInvestigators(List<Location> locList)
    {
        List<Investigator> lsinv = new ArrayList<Investigator>();
        int lowestSneak = 99999;
        for (Location adjLoc : locList)
        {
            if (!adjLoc.isStreet())
            {
                /* Flying monsters can only fly into streets */
                continue;
            }
            for (Investigator inv : adjLoc.getInvestigatorList())
            {
                if (inv.getCurrentSneak() < lowestSneak)
                {
                    lsinv.clear();
                    lsinv.add(inv);
                    lowestSneak = inv.getCurrentSneak();
                }
                else if (inv.getCurrentSneak() == lowestSneak)
                {
                    lsinv.add(inv);
                }
            }
        }
        if (lsinv.size() > 1)
        {
            /* if there is more than one investigator with the lowest
             * sneak, check if they are all on the same location */
            for (int i = 0; i < lsinv.size(); i++)
            {
                for (int j = i + 1; j < lsinv.size(); j++)
                {
                    String locStr1 = lsinv.get(i).getLocationName();
                    String locStr2 = lsinv.get(j).getLocationName();
                    if (locStr1.equals(locStr2))
                    {
                        lsinv.remove(j);
                        j--;
                    }
                }
            }
        }
        return lsinv;
    }

    /**
     * Called during pre-upkeep. This method refreshes all investigator cards,
     * status effects, and powers, and removes the "dead" status, if any
     */
    public void refreshInvestigators()
    {
        for (int i = 0; i < investigators.length; i++)
        {
            investigators[i].refreshCards();
            investigators[i].setDead(false);
        }
    }

    /**
     * Prepares the investigator for the movement phase.
     * @param inv the investigator to prepare for
     */
    public void prepareForMovement(Investigator inv)
    {
        List<EffectType> envBonusList = searchInvestigatorGlobalEffects(inv);
        // Check for environments that add Speed or Movement bonuses/penalties
        GameData.filterEffectList(envBonusList, "Skill Bonus");
        int mod = 0;
        for (EffectType eff : envBonusList)
        {
            if (eff.getDetails().equals("Speed") || eff.getDetails().equals("Movement"))
            {
                mod += Integer.parseInt(eff.getAmount());
            }
        }
        inv.setMovementEffectsLocked(false);
        inv.setMovementPointsLocked(false);
        inv.setExploredMarker(false);
        inv.setAvoidedMonsters(false);
        inv.setMovementPoints(Math.max(0, inv.getCurrentSpeed() + mod));

        if (inv.isDelayed())
        {
            inv.setMovementEffectsLocked(true);
            inv.setMovementPointsLocked(true);
        }
        if (!inv.getLocation().isArkhamLocation())
        {
            inv.setMovementPointsLocked(true);
        }
        if (inv.areMovementPointsLocked())
        {
            inv.setMovementPoints(0);
        }
    }

    /**
     * Ends this investigator's movement, and determines the movement state.<br>
     * If the investigator is delayed, removes the delay.<br>
     * If the investigator is in Arkham, ends movement and collects all clues at
     * the location, if any.<br>
     * If the investigator is in an Other World, moves the investigator forward<br>
     * one spot, possibly re-entering Arkham.
     * @param inv the investigator to move
     * @return the movestate to enter
     */
    public MoveState endMovement(Investigator inv)
    {
        if (inv.isDelayed())
        {
            inv.setDelayed(false);
            return MoveState.NONE;
        }
        else if (inv.getLocation().isArkhamLocation())
        {
            return MoveState.NONE;
        }
        else
        {
            Pair<OtherWorld, Integer> pair = parseOWLocation(inv.getLocationName());
            if (pair != null)
            {
                if (pair.getB() == 1)
                {
                    moveInvestigatorTo(inv, pair.getA().getSegment(2));
                    return MoveState.NONE;
                }
                else
                {
                    /* Investigator is returning to Arkham from an Other World.
                     * Search for open gates. */
                    List<Location> list = getOpenGatesTo(pair.getA().getName());
                    if (list.size() == 0)
                    {
                        /* LITAS */
                        moveInvestigatorTo(inv, mainBoard.getLocationContainer(Board.LITAS_NAME));
                        return MoveState.NONE;
                    }
                    else if (list.size() == 1)
                    {
                        moveInvestigatorTo(inv, list.get(0));
                        inv.setExploredMarker(true);
                        return MoveState.JUST_RETURNED;
                    }
                    else
                    {
                        return MoveState.CHOOSE_RETURN_GATE;
                    }
                }
            }
            else
            {
                AHLogger.warning("EndMovement", "Unable to determine investigator's" +
                		" movement status: " + inv.getName() + ", " + inv.getLocationName());
                return null;
            }
        }
    }

    /**
     * @return the combatFrame
     */
    public List<Monster> getCombatFrame()
    {
        return combatFrame;
    }

    /**
     * Clears the combat frame
     */
    public void clearCombatFrame()
    {
        combatFrame.clear();
    }

    /**
     * Prepares the combat frame for combat
     * @param monsters the list of monsters
     */
    public void prepareCombatFrame(List<Monster> monsters)
    {
        combatFrame.clear();
        combatFrame.addAll(monsters);
    }

    /**
     * Removes this monster from the combat frame
     * @param mon the monster to remove
     */
    public void removeFromCombatFrame(Monster mon)
    {
        removeById(combatFrame, mon.getId());
    }

    /**
     * Updates the combat frame with monsters
     * @param details the details provided
     */
    public void updateCombatFrame(String details)
    {
        combatFrame.clear();
        String[] tokens = details.split(",");
        for (String tok : tokens)
        {
            int id = Integer.parseInt(tok);
            Monster mon = getMonsterById(id);
            combatFrame.add(mon);
        }
    }

    /**
     * Sets the current trade recipient
     * @param tradeRecipient the tradeRecipient to set
     */
    public void setTradeRecipient(Investigator tradeRecipient)
    {
        this.tradeRecipient = tradeRecipient;
    }

    /**
     * Gets the current trade recipient
     * @return the tradeRecipient
     */
    public Investigator getTradeRecipient()
    {
        return tradeRecipient;
    }

    /**
     * Sets the trade details
     * @param tradeDetails the trade details
     */
    public void setTradeDetails(String tradeDetails)
    {
        this.tradeDetails = tradeDetails;
    }

    /**
     * Gets the trade details
     * @return the trade details
     */
    public String getTradeDetails()
    {
        return tradeDetails;
    }

    /**
     * Validates a trade string. Checks if string is well formed and if the items traded
     * belong to the respective investigators.
     * @param details the details
     * @param inv the trade initiator
     * @param tinv the trade recipient
     * @param list1 optionally, a list provided to store the cards the initiator is trading
     * @param list2 optionally, a list provided to store the cards the recipient is trading
     * @return true if the trade string is valid, false otherwise
     */
    public boolean validateTrade(String details, Investigator inv, Investigator tinv,
            List<InvestigatorCard> list1, List<InvestigatorCard> list2)
    {
        String[] sides = details.split(";", -1);
        if (!sides[0].equals(""))
        {
            String[] tokens = sides[0].split(",");
            for (String tok : tokens)
            {
                if (!XmlTools.isNumeric(tok))
                {
                    return false;
                }
                int cardid = Integer.parseInt(tok);
                InvestigatorCard card = GameData.retrieveById
                    (inv.getInvcards(), cardid);
                if (card == null)
                {
                    return false;
                }
                if (list1 != null)
                {
                    list1.add(card);
                }
            }
        }
        if (!sides[1].equals(""))
        {
            String[] tokens = sides[1].split(",");
            for (String tok : tokens)
            {
                if (!XmlTools.isNumeric(tok))
                {
                    return false;
                }
                int cardid = Integer.parseInt(tok);
                InvestigatorCard card = GameData.retrieveById
                    (tinv.getInvcards(), cardid);
                if (card == null)
                {
                    return false;
                }
                if (list2 != null)
                {
                    list2.add(card);
                }
            }
        }
        return true;
    }

    /**
     * Trades items between two investigators
     * @param inv1 the first investigator
     * @param inv2 the second investigator
     * @param list1 the items traded by the first investigator
     * @param list2 the items traded by the second investigator
     */
    public void tradeItems(Investigator inv1, Investigator inv2,
            List<InvestigatorCard> list1, List<InvestigatorCard> list2)
    {
        for (InvestigatorCard card : list1)
        {
            inv1.loseInvestigatorCard(card);
            inv2.gainInvestigatorCard(card);
        }
        for (InvestigatorCard card : list2)
        {
            inv2.loseInvestigatorCard(card);
            inv1.gainInvestigatorCard(card);
        }
    }

    /**
     * Gets the current event, if any
     * @return the current event, or null if not selected
     */
    public EventNode getCurrentEvent()
    {
        return currentEvent;
    }

    /**
     * Sets the current event
     * @param event the event to set
     */
    public void setCurrentEvent(EventNode event)
    {
        this.currentEvent = event;
    }

    /**
     * Clears the current event if any
     */
    public void clearCurrentEvent()
    {
        currentEvent = null;
    }

    /**
     * Draws an event from that location deck. If the location deck for this
     * neighborhood has a face up card, that one will be drawn
     * @param locName the location name
     * @param neighborhood the neighborhood name
     * @return an event from this location at random
     */
    public EventNode drawEventFromLocation(String locName, String neighborhood)
    {
        Deck<EventNode> deck = locationCards.get(locName);
        if (deck == null)
        {
            AHLogger.error("drawEvent", "No deck for location " + locName);
            return null;
        }
        if (faceupEncounterNeighborhood != null && neighborhood.equals(faceupEncounterNeighborhood))
        {
            AHLogger.info("drawEvent",
                    "The face up card for " + neighborhood + " has been drawn.");
            faceupEncounterNeighborhood = null;
            return deck.revealTop();
        }
        return deck.revealRandom();
    }

    /**
     * Draws multiple events from the neighborhood and adds it to a list so that the
     * investigator may make a choice
     * @param locName the location name
     * @param neighborhood the neighborhood name
     * @param choices the number of events to draw
     * @param encounters the list of encounters
     */
    public void drawEventsFromLocation(String locName, String neighborhood, int choices,
            List<EventNode> encounters)
    {
        Deck<EventNode> deck = locationCards.get(locName);
        List<EventNode> temp = new ArrayList<EventNode>();
        if (deck == null)
        {
            AHLogger.error("drawEvents", "No deck for location " + locName);
        }
        if (faceupEncounterNeighborhood != null && neighborhood.equals(faceupEncounterNeighborhood))
        {
            choices--;
            faceupEncounterNeighborhood = null;
            AHLogger.info("drawEvent", "The face up card for " + neighborhood + " has been drawn.");
            temp.add(deck.deal());
        }
        for (int i = 0; i < choices; i++)
        {
            temp.add(deck.randomDeal());
        }
        encounters.addAll(temp);
        while (!temp.isEmpty())
        {
            deck.addToBottom(temp.remove(temp.size() - 1));
        }
    }

    /**
     * Draws a specific location card given the location and the encounter name
     * @param locName the location name
     * @param encounterName the name of the encounter
     */
    public EventNode drawLocationCard(String locName, String encounterName)
    {
        Deck<EventNode> deck = locationCards.get(locName);
        if (deck == null)
        {
            AHLogger.error("drawEvents", "No deck for location " + locName);
        }
        return EventUtils.getEventByName(deck, encounterName);
    }

    /**
     * Draws gate cards and discards them to the bottom of the deck, returning the
     * first X encounters which match the gate colors.
     * @param ow The other world to draw in
     * @param choices the number of choices
     * @param selectedCards the list to store selected encounters in
     */
    public void drawGateCards(OtherWorld ow, int choices, List<GateCard> selectedCards)
    {
        while (selectedCards.size() < choices)
        {
            GateCard card = gateCards.deal();
            if (ow.getColorSet().hasColor(card.getGateColor()))
            {
                selectedCards.add(card);
            }
            gateCards.addToBottom(card);
        }
    }

    /**
     * Gets a gate card by name. Does not change the order of the deck
     * @param name the name of the gate card to get
     * @return the gate card with the name or null if not found
     */
    public GateCard getGateCardByName(String name)
    {
        GateCard card = Deck.viewSpecificCard(gateCards, name);
        return card;
    }

    /* --------------------- Global Variable Methods ---------------- */

    /**
     * Gets the gate closing difficulty
     * @return the difficulty to close gates
     */
    public int getGateClosingDifficulty(GateTile gate)
    {
        int diff = 1;
        if (ancientOne.getName().equalsIgnoreCase("Yog-Sothoth"))
        {
            diff += 1;
        }
        List<EffectType> enveffects = searchGateGlobalEffects(gate);
        filterEffectList(enveffects, "Gate Closing/Sealing", "Difficulty");
        for (EffectType eff : enveffects)
        {
            int amt = Integer.parseInt(eff.getAmount());
            diff += amt;
        }
        return Math.max(1, diff);
    }

    /**
     * Gets the number of clues needed to seal a gate
     * @return the number of clues to seal a gate
     */
    public int getNumCluesToSeal()
    {
        // TODO: consider Noden's Favor environment
        if (ancientOne.getName().equals("Hastur"))
        {
            return 8;
        }
        return 5;
    }

    /* ------------- Utility Methods --------------- */
    /**
     * Load a JAXB element from a file
     * @param <E> The type to load
     * @param file The file to load from
     * @param pkgname The name of the package to load from
     * @return The JAXB element if successful, null on a failure
     */
    @SuppressWarnings("unchecked")
    private <E> E loadJAXBElement(File file, String pkgname)
    {
        try
        {
            JAXBContext jc = JAXBContext.newInstance(pkgname);

            Unmarshaller um = jc.createUnmarshaller();
            Object obj = um.unmarshal(file);
            JAXBElement<E> elem = (JAXBElement<E>) obj;
            E val = elem.getValue();
            return val;
        }
        catch (JAXBException e)
        {
            AHLogger.error("GameData.loadJAXBElement", "Error with loading " +
                    file.getName() + ": " + e.getMessage());
            return null;
        }
    }

    /**
     * Load a JAXB element from a string
     * @param <E> The type to load
     * @param xmltext The text to load from
     * @param pkgname The name of the package to load from
     * @return The JAXB element if successful, null on a failure
     */
    @SuppressWarnings("unchecked")
    private <E> E loadJAXBElement(String xmltext, String pkgname)
    {
        try
        {
            JAXBContext jc = JAXBContext.newInstance(pkgname);

            Unmarshaller um = jc.createUnmarshaller();
            Object obj = um.unmarshal(new StringReader(xmltext));
            JAXBElement<E> elem = (JAXBElement<E>) obj;
            E val = elem.getValue();
            return val;
        }
        catch (JAXBException e)
        {
            e.printStackTrace();
            AHLogger.error("GameData.loadJAXBElement", "Error with loading " +
                     ": " + e.getMessage());
            return null;
        }
    }

    /**
     * Checks across all boards to see if a location name is valid
     * @param locName The location name
     * @return true if it exists
     */
    private boolean isLocationValid(String locName)
    {
        if (mainBoard.hasLocation(locName)) return true;
        if (locName.equals("Inner Sanctum")) return true;
        for (Board board : expansionBoards)
        {
            if (board.hasLocation(locName)) return true;
        }
        return false;
    }

    /**
     * Checks to see if an other world location is valid
     * @param owName The other world location name
     * @return true if it exists
     */
    private boolean isOtherWorldValid(String owName)
    {
        return otherWorldLocations.contains(owName);
    }

    /**
     * Parses an other world location's name, returning the other world
     * and the segment specified as a pair
     * @param locName the location name
     * @return the other world and segment, if it exists, or null otherwise
     */
    public Pair<OtherWorld, Integer> parseOWLocation(String locName)
    {
        int index = locName.indexOf(':');
        if (index != -1)
        {
            String owname = locName.substring(0, index);
            String segnum = locName.substring(index + 1);
            try
            {
                if (isOtherWorldValid(owname))
                {
                    return new Pair<OtherWorld, Integer>(getOtherWorld(owname),
                            Integer.parseInt(segnum));
                }
            }
            catch (NumberFormatException e)
            {
                AHLogger.warning("parseOWLocation", "Invalid OW: " + locName);
                return null;
            }
        }
        return null;
    }

    /**
     * Checks if a monster's location is valid. Monsters can be in
     * board locations, outskirts and sky
     * @param locName the location name
     * @return the location
     */
    private boolean isMonsterLocationValid(String locName)
    {
        if (isLocationValid(locName)) return true;
        if (Board.SKY_NAME.equals(locName)) return true;
        if (Board.OUTSKIRTS_NAME.equals(locName)) return true;
        return false;
    }

    /**
     * Checks if an investigator's location is valid. Investigators can be in
     * board locations, other worlds, and LITAS
     * @param locName the location name
     * @return the location
     */
    private boolean isInvestigatorLocationValid(String locName)
    {
        if (isLocationValid(locName)) return true;
        if (isOtherWorldValid(locName)) return true;
        if (Board.LITAS_NAME.equals(locName)) return true;

        return false;
    }

    /**
     * Gets the appropriate location across all boards
     * @param name the location name
     * @return the location, or null if not found
     */
    public Location getLocation(String name)
    {
        Location loc = mainBoard.getLocation(name);
        if (loc == null)
        {
            for (Board b : expansionBoards)
            {
                loc = b.getLocation(name);
                if (loc != null)
                {
                    return loc;
                }
            }
        }
        return loc;
    }

    /**
     * This method retrieves the hospital location
     * @return the location of the hospital
     */
    public Location getHospitalLocation()
    {
        return getLocation("St. Mary's Hospital");
    }

    /**
     * This method retrieves the asylum location
     * @return the location of the asylum
     */
    public Location getAsylumLocation()
    {
        return getLocation("Arkham Asylum");
    }

    /**
     * Gets the appropriate other world across all boards
     * @param name the name of the other world
     * @return the other world if found, or null otherwise
     */
    public OtherWorld getOtherWorld(String name)
    {
        if (!isOtherWorldValid(name))
        {
            return null;
        }
        OtherWorld ow = mainBoard.getOtherWorld(name);
        if (ow == null)
        {
            for (Board b : expansionBoards)
            {
                ow = b.getOtherWorld(name);
                if (ow != null)
                {
                    return ow;
                }
            }
        }
        return ow;
    }

    /**
     * Gets the appropriate location container across all boards
     * @param name the name of the container
     * @return the location container, or null if not found
     */
    public LocationContainer getLocationContainer(String name)
    {
        for (LocationContainer lc : allLocations)
        {
            if (lc.getName().equals(name))
            {
                return lc;
            }
        }
        return null;
    }

    /**
     * Gets a list of all locations across all boards with open gates to a
     * specified other world
     * @param owname the other world name
     * @return the list of locations with open gates
     */
    public List<Location> getOpenGatesTo(String owname)
    {
        List<Location> list = mainBoard.getOpenGatesTo(owname);
        for (Board b : expansionBoards)
        {
            list.addAll(b.getOpenGatesTo(owname));
        }
        return list;
    }

    /**
     * Checks if anyone with a certain modeffect is present at a location
     * @param loc the location to check
     * @param type the type of the modeffect
     * @param details the details of the modeffect
     * @return true if an investigator with a power is a that location
     */
    public boolean locationCheck(Location loc, String type, String details)
    {
        for (Investigator inv : loc.getInvestigatorList())
        {
            if (inv.hasModifier(type, details))
            {
                return true;
            }
        }
        return false;
    }

    /**
     * Sets up two data structures, a mapping of location to index
     * and a matrix of distances between locations, and then computes
     * the distance between any two locations on the board.
     */
    private void computeDistances()
    {
        arkhamLocations = new ArrayList<Location>();
        arkhamLocations.addAll(mainBoard.getLocations());
        locationIndices = new HashMap<Location, Integer>();
        for (Board b : expansionBoards)
        {
            arkhamLocations.addAll(b.getLocations());
        }
        for (int i = 0; i < arkhamLocations.size(); i++)
        {
            locationIndices.put(arkhamLocations.get(i), i);
        }
        distanceMatrix = new int[arkhamLocations.size()][arkhamLocations.size()];
        for (int i = 0; i < distanceMatrix.length; i++)
        {
            for (int j = 0; j < distanceMatrix.length; j++)
            {
                if (i == j)
                {
                    distanceMatrix[i][j] = 0;
                }
                else
                {
                    Location loc1 = arkhamLocations.get(i);
                    Location loc2 = arkhamLocations.get(j);
                    if (loc1.hasEdgeTo(loc2.getName()))
                    {
                        distanceMatrix[i][j] = 1;
                    }
                    else if (loc1.isStation() && loc2.isStation())
                    {
                        distanceMatrix[i][j] = 1;
                    }
                    else
                    {
                        distanceMatrix[i][j] = 99999;
                    }
                }
            }
        }
        for (int k = 0; k < distanceMatrix.length; k++)
        {
            for (int i = 0; i < distanceMatrix.length; i++)
            {
                for (int j = 0; j < distanceMatrix.length; j++)
                {
                    distanceMatrix[i][j] = Math.min(distanceMatrix[i][j],
                            distanceMatrix[i][k] + distanceMatrix[k][j]);
                }
            }
        }
        /* This code prints out the distance matrix
        System.out.print("       ");
        for (int i = 0; i < distanceMatrix.length; i++)
        {
            System.out.print(allLocations.get(i).getName().substring(0, 2) + " ");
        }
        System.out.println();
        for (int i = 0; i < distanceMatrix.length; i++)
        {
            System.out.print(allLocations.get(i).getName().substring(0, 5) + "  ");
            for (int j = 0; j < distanceMatrix.length; j++)
            {
                System.out.print(XmlTools.pad("" + distanceMatrix[i][j], 3));
            }
            System.out.println();
        }
        */
    }

    /**
     * Calculates the distance between any two locations
     * @param loc1 The first location
     * @param loc2 The second location
     * @return the distance between them in number of squares, or -1 if not found
     */
    public int computeDistance(Location loc1, Location loc2)
    {
        Integer index1 = locationIndices.get(loc1);
        Integer index2 = locationIndices.get(loc2);
        if (index1 == null || index2 == null)
        {
            return -1;
        }
        if (index1 >= 0 && index1 < distanceMatrix.length && index2 >= 0 &&
                index2 < distanceMatrix[index1].length)
        {
            return distanceMatrix[index1][index2];
        }
        return -1;
    }

    /**
     * Computes the best path between two locations, given the limit of
     * maximum length of the path.<br>
     * 1. Finds the shortest clear path to the destination, within the
     *    movement limit.<br>
     * 2. If no clear path is available, finds the shortest path with
     *    enemies. Any such path will do.<br>
     * @param src the first location
     * @param dest the second location
     * @param limit the maximum length of the path
     * @param avoided set to true if monsters have already been avoided on the
     * current location
     * @return the computed path or null if either location doesn't exist or no
     * path exists within that limit.
     */
    public Path computePath(Location src, Location dest, int limit, boolean avoided)
    {
       Path path = new Path(src);
       /* run a BFS without enemies */
       Integer index1 = locationIndices.get(src);
       Integer index2 = locationIndices.get(dest);
       if (index1 == null || index2 == null)
       {
           return null;
       }
       if (index1 == index2)
       {
           return path;
       }
       LinkedList<IntPair> queue = new LinkedList<IntPair>();
       if (src.getMonsterList().size() == 0 || avoided)
       {
           queue.offer(new IntPair(index1, 0));
       }
       boolean[] visited = new boolean[distanceMatrix.length];
       boolean[] infested = new boolean[distanceMatrix.length];
       int[] rev = new int[distanceMatrix.length];
       Arrays.fill(visited, false);
       Arrays.fill(rev, -1);
       int found = -1;
       while (!queue.isEmpty())
       {
           IntPair ipair = queue.poll();
           visited[ipair.getA()] = true;
           if (ipair.getA() == index2)
           {
               found = ipair.getB();
               break;
           }
           if (ipair.getB() > limit)
           {
               break;
           }
           for (int i = 0; i < distanceMatrix.length; i++)
           {
               if (visited[i]) continue;
               if (distanceMatrix[ipair.getA()][i] != 1) continue;
               if (arkhamLocations.get(i).getMonsterList().size() > 0)
               {
                   continue;
               }
               queue.offer(new IntPair(i, ipair.getB() + 1));
               if (rev[i] == -1)
               {
                   rev[i] = ipair.getA();
               }
           }
       }
       if (found != -1)
       {
           int td = index2;
           for (int i = found - 1; i >= 0; i--)
           {
               path.getPath().add(0, arkhamLocations.get(td));
               td = rev[td];
           }
           return path;
       }
       /* Perform another BFS but with monsters */
       Arrays.fill(visited, false);
       Arrays.fill(infested, false);
       Arrays.fill(rev, -1);
       queue.clear();
       queue.offer(new IntPair(index1, 0));

       while (!queue.isEmpty())
       {
           IntPair ipair = queue.poll();
           visited[ipair.getA()] = true;
           if (ipair.getA() == index2)
           {
               found = ipair.getB();
               break;
           }
           if (ipair.getB() > limit)
           {
               break;
           }
           for (int i = 0; i < distanceMatrix.length; i++)
           {
               if (visited[i]) continue;
               if (distanceMatrix[ipair.getA()][i] != 1) continue;
               if (infested[ipair.getA()] ||
                       (arkhamLocations.get(ipair.getA()).getMonsterList().size() > 0
                               && (ipair.getA() != index1 || !avoided)))
               {
                   infested[i] = true;
               }
               queue.offer(new IntPair(i, ipair.getB() + 1));
               if (rev[i] == -1)
               {
                   rev[i] = ipair.getA();
               }
           }
       }
       if (found != -1)
       {
           int td = index2;
           for (int i = found - 1; i >= 0; i--)
           {
               path.getPath().add(0, arkhamLocations.get(td));
               if (infested[td])
               {
                   path.setBlockedNodes(path.getBlockedNodes() + 1);
               }
               td = rev[td];
           }
           return path;
       }
       return null;
    }

    /**
     * Searches the appropriate deck for the first card with that
     * name and deals it.
     * @param cardName The card name
     * @return the card dealt, if it exists
     */
    public InvestigatorCard dealFirstSpecificCard(String cardName)
    {
        InvestigatorCardType cardType = lookupCardDatabase(cardName).getType();
        InvestigatorCard card = null;
        switch (cardType)
        {
        case COMMON:
            card = Deck.dealSpecificCard(commonDeck, cardName);
            break;
        case UNIQUE:
            card = Deck.dealSpecificCard(uniqueDeck, cardName);
            break;
        case SPELL:
            card = Deck.dealSpecificCard(spellDeck, cardName);
            break;
        case SKILL:
            card = Deck.dealSpecificCard(skillDeck, cardName);
            break;
        case ALLY:
            card = Deck.dealSpecificCard(allyDeck, cardName);
            break;
        }
        return card;
    }

    /**
     * Consumes the activation cost of a mod effect given the identifier string
     * @param inv the investigator which has the modifier
     * @param modString the mod identifier string
     */
    public void consumeModEffect(Investigator inv, String modString)
    {
        String[] tokens = modString.split(",");
        int numUses = (tokens.length == 4) ? Integer.parseInt(tokens[3]) : 1;
        List<ModEffect> list = inv.searchModifiers(tokens[1], tokens[2]);
        for (ModEffect mod : list)
        {
            if (mod.getSourceString().equals(tokens[0]))
            {
                consumeEffect(inv, mod, numUses);
            }
        }
    }

    /**
     * Consumes the activation cost of this mod effect.
     * @param inv the investigator owning the effect
     * @param mod the mod effect
     */
    public void consumeModEffect(Investigator inv, ModEffect mod)
    {
        consumeEffect(inv, mod, 1);
    }

    /**
     * Consumes the activation cost of this usable effect given the identifier string
     * @param inv the investigator owning the effect
     * @param details the identifier string
     */
    public void consumeUsableEffect(Investigator inv, String details)
    {
        String[] tokens = details.split(",");
        UsableEffect ueff = inv.searchUsableEffect(tokens[0], Integer.parseInt(tokens[1]));
        if (ueff != null)
        {
            consumeUsableEffect(inv, ueff);
        }
    }

    /**
     * Consumes the activation cost of this usable effect
     * @param inv the investigator owning the effect
     * @param ueff the usable effect
     */
    public void consumeUsableEffect(Investigator inv, UsableEffect ueff)
    {
        consumeEffect(inv, ueff, 1);
    }

    /**
     * Consumes the activation cost of this effect.
     * @param inv the investigator which has the modifier
     * @param eff the effect
     * @param numUses the number of times this effect is used,
     * if it used multiple times on a LIMITED item
     */
    public void consumeEffect(Investigator inv, Effect eff, int numUses)
    {
        switch (eff.getUsage())
        {
        case CAST_AND_EXHAUST:
            Spell spell = (Spell)eff.getSource();
            inv.modifySanity(-spell.getSanityCost());
        case EXHAUST:
            eff.getSource().setExhausted(true);
            break;
        case CAST_AND_DISCARD:
            spell = (Spell)eff.getSource();
            inv.modifySanity(-spell.getSanityCost());
        case DISCARD:
            if (eff.getSource() instanceof InvestigatorCard)
            {
                int cardId = ((InvestigatorCard)eff.getSource()).getId();
                InvestigatorCard card = GameData.retrieveById(inv.getInvcards(), cardId);
                inv.loseInvestigatorCard(card);
                returnToDeck(card);
            }
            else if (eff.getSource() instanceof PersonalEffect)
            {
                inv.removeStatus(eff.getSource().getName());
            }
            break;
        case LIMITED:
        case LIMITED_MULTI:
            eff.getSource().setNumCounters(eff.getSource().getNumCounters() + numUses);
            if (eff.getSource().getNumCounters() >= eff.getSource().getLimit()
                    && eff.getSource().getLimit() > 0)
            {
                if (eff.getSource() instanceof InvestigatorCard)
                {
                    int cardId = ((InvestigatorCard)eff.getSource()).getId();
                    InvestigatorCard card = GameData.retrieveById(inv.getInvcards(), cardId);
                    inv.loseInvestigatorCard(card);
                    returnToDeck(card);
                }
                else if (eff.getSource() instanceof PersonalEffect)
                {
                    inv.removeStatus(eff.getSource().getName());
                }
            }
            break;
        case RETURN_BOX:
            if (eff.getSource() instanceof InvestigatorCard)
            {
                int cardId = ((InvestigatorCard)eff.getSource()).getId();
                GameData.removeById(inv.getInvcards(), cardId);
            }
            break;
        default:
            break;
        }

        for (CostType cost : eff.getExtraCosts())
        {
            if (cost.getType().equals("Movement"))
            {
                inv.useMovementPoints(Integer.parseInt(cost.getAmount()));
            }
            else if (cost.getType().equals("Stamina"))
            {
                inv.modifyStamina(-Integer.parseInt(cost.getAmount()));
            }
            else if (cost.getType().equals("Sanity"))
            {
                inv.modifySanity(-Integer.parseInt(cost.getAmount()));
            }
        }
    }

    /**
     * Add tokens to an effect source belonging to an investigator, discarding the
     * item if there are more tokens than the limit
     * @param inv the investigator who owns the effect source
     * @param source the effect source
     * @param numTokens the number of tokens to add
     */
    public void addTokensToEffectSource(Investigator inv, EffectSource source, int numTokens)
    {
        source.setNumCounters(source.getNumCounters() + numTokens);
        if (source.getNumCounters() >= source.getLimit()
                && source.getLimit() > 0)
        {
            if (source instanceof InvestigatorCard)
            {
                InvestigatorCard card = (InvestigatorCard)source;
                inv.loseInvestigatorCard(card);
                returnToDeck(card);
            }
            else if (source instanceof PersonalEffect)
            {
                inv.removeStatus((PersonalEffect)source);
            }
        }
    }

    /**
     * Makes an "itemcheck", which checks for whether a certain condition can be met
     * @param inv the investigator in question
     * @param cost the type of the check
     * @param amount the amount to check for
     * @return true if the itemcheck is passed
     */
    public boolean makeItemcheck(Investigator inv, String cost, String amount)
    {
        if (cost.startsWith("Ally Available:"))
        {
            String allyName = cost.substring(15);
            Ally ally = Deck.viewSpecificCard(allyDeck, allyName);
            return ally != null;
        }
        else
        {
            return canPayCost(inv, cost, amount);
        }
    }

    /**
     * Checks if an investigator can pay a particular cost
     * @param cost the cost to pay
     * @param amount the amount of the cost
     * @return true if the investigator can pay the cost, or there is no cost
     */
    public boolean canPayCost(Investigator inv, String cost, String amount)
    {
        if (cost.equals("None"))
        {
            return true;
        }
        AmountParser apar = new AmountParser(amount);
        if (cost.equals("Clue"))
        {
            return inv.getClues() >= apar.getNumericAmount(inv.getClues());
        }
        else if (cost.equals("Money"))
        {
            return inv.getMoney() >= apar.getNumericAmount(inv.getMoney());
        }
        else if (cost.equals("Item"))
        {
            return inv.getItems().size() >= apar.n;
        }
        else if (InvestigatorCardType.isValidCardType(cost))
        {
            InvestigatorCardType cardType = InvestigatorCardType.getCardType(cost);
            return inv.getCardsOfType(cardType).size() >= apar.n;
        }
        else if (cost.startsWith("Specific Card:"))
        {
            String cardName = cost.substring(14);
            InvestigatorCard card = retrieveByName(inv.getInvcards(), cardName);
            return card != null;
        }
        else if (cost.equals("Monster Trophy"))
        {
            List<Monster> trophies = inv.getMonsterTrophies();
            int sum = 0;
            for (Monster mon : trophies)
            {
                sum += getModifiedStat(mon, Monster.STAT_TOUGHNESS);
            }
            return sum >= apar.n;
        }
        else if (cost.equals("Gate Trophy"))
        {
            return inv.getGateTrophies().size() >= apar.n;
        }
        else if (cost.equals("Sanity"))
        {
            return inv.getCurrentSanity() >= apar.getNumericAmount(inv.getCurrentSanity());
        }
        else if (cost.equals("Stamina"))
        {
            return inv.getCurrentStamina() >= apar.getNumericAmount(inv.getCurrentStamina());
        }
        else
        {
            AHLogger.error("canPayCost", "Invalid cost: " + cost);
            return false;
        }
    }

    /**
     * Gets the first occurrence of an entity with a certain name in a list
     * @param <E> the type of the named entity
     * @param list the list to get from
     * @param name the name to find
     * @return the first item with that name, or null if not found
     */
    public static <E extends NamedEntity> E retrieveByName(List<E> list, String name)
    {
        for (E item : list)
        {
            if (item.getName().equals(name))
            {
                return item;
            }
        }
        return null;
    }

    /**
     * Removes the first occurrence of an entity with a certain name in a list
     * @param <E> the type of the named entity
     * @param list the list to get from
     * @param name the name to find
     * @return the first item with that name, or null if not found
     */
    public static <E extends NamedEntity> E removeByName(List<E> list, String name)
    {
        for (int i = 0; i < list.size(); i++)
        {
            if (list.get(i).getName().equals(name))
            {
                return list.remove(i);
            }
        }
        return null;
    }

    /**
     * Removes all occurrences of an entity with a certain name in a list
     * @param <E> the type of the named entity
     * @param list the list to get from
     * @param name the name to find
     * @param removedList if not null, places all removed entries in this list, otherwise,
     * creates a new removed list to store removed entries
     * @return the list of all removed entries
     */
    public static <E extends NamedEntity> List<E> removeAllByName(List<E> list, String name,
            List<E> removedList)
    {
        if (removedList == null)
        {
            removedList = new ArrayList<E>();
        }
        for (int i = 0; i < list.size(); i++)
        {
            if (list.get(i).getName().equals(name))
            {
                removedList.add(list.remove(i));
            }
        }
        return removedList;
    }

    /**
     * Gets the first occurrence of an entity with a certain id in a list
     * @param <E> the type of the named entity
     * @param list the list to get from
     * @param id the id to find
     * @return the first item with that name, or null if not found
     */
    public static <E extends IdNumbered> E retrieveById(List<E> list, int id)
    {
        for (E item : list)
        {
            if (item.getId() == id)
            {
                return item;
            }
        }
        return null;
    }

    /**
     * Removes the first occurrence of an entity with a certain id in a list
     * @param <E> the type of the named entity
     * @param list the list to get from
     * @param id the id to find
     * @return the first item with that name, or null if not found
     */
    public static <E extends IdNumbered> E removeById(List<E> list, int id)
    {
        for (int i = 0; i < list.size(); i++)
        {
            if (list.get(i).getId() == id)
            {
                return list.remove(i);
            }
        }
        return null;
    }

    /**
     * Removes the last occurrence of an entity with a certain name in a list
     * @param <E> the type of the named entity
     * @param list the list to get from
     * @param name the name to find
     * @return the last item with that name, or null if not found
     */
    public static <E extends NamedEntity> E removeByNameReverse(List<E> list, String name)
    {
        for (int i = list.size() - 1; i >= 0; i--)
        {
            if (list.get(i).getName().equals(name))
            {
                return list.remove(i);
            }
        }
        return null;
    }

    /**
     * Removes the last occurrence of an entity with a certain id in a list
     * @param <E> the type of the idnumbered entity
     * @param list the list to get from
     * @param id the id to find
     * @return the last item with that id, or null if not found
     */
    public static <E extends IdNumbered> E removeByIdReverse(List<E> list, int id)
    {
        for (int i = list.size() - 1; i >= 0; i--)
        {
            if (list.get(i).getId() == id)
            {
                return list.remove(i);
            }
        }
        return null;
    }

    /**
     * Reorders the list by name by shuffling the list to fit the name array. The
     * list and the array must be of the same length
     * @param <E> the type of the named entity
     * @param list the list to reorder
     * @param names the array of names
     */
    public static <E extends NamedEntity> void reorderByName(List<E> list, String[] names)
    {
        for (int i = 0; i < names.length; i++)
        {
            E item = removeByNameReverse(list, names[i]);
            list.add(i, item);
        }
    }

    /**
     * Reorders the list by id by shuffling the list to fit the id array. The
     * list and the array must be of the same length
     * @param <E> the type of the idnumbered entity
     * @param list the list to reorder
     * @param names the array of ids, in string form
     */
    public static <E extends IdNumbered> void reorderById(List<E> list, String[] ids)
    {
        for (int i = 0; i < ids.length; i++)
        {
            E item = removeByIdReverse(list, Integer.parseInt(ids[i]));
            list.add(i, item);
        }
    }

    /**
     * Count the number of occurences of a value in a list
     * @param <E> the list type
     * @param list the list to search
     * @param value the value to count occurences of
     * @return the number of occurences
     */
    public static <E> int countOccurences(List<E> list, E value)
    {
        int sum = 0;
        for (int i = 0; i < list.size(); i++)
        {
            if (list.get(i).equals(value))
            {
                sum++;
            }
        }
        return sum;
    }

    /**
     * Removes all effects that are unavailable or unusable due to being exhausted,
     * an always-on effect, or the investigator not being able to pay the cost
     * @param inv the investigator
     * @param effList the list of effects
     */
    public static void removeUnavailableEffects(Investigator inv, List<? extends Effect> effList)
    {
        removeExhaustedEffects(effList);
        removeUnusableEffects(inv, effList);
    }

    /**
     * Removes all effects that are not available because they are
     * either ALWAYS active, or an EXHAUST type effect that is already
     * exhausted.
     * @param effList the list of mod effects
     */
    private static void removeExhaustedEffects(List<? extends Effect> effList)
    {
        for (int i = 0; i < effList.size(); i++)
        {
            Effect eff = effList.get(i);
            boolean valid = true;
            if (eff.getUsage().equals(EffectUsage.ALWAYS))
            {
                valid = false;
            }
            else if ((eff.getUsage().equals(EffectUsage.CAST_AND_EXHAUST) ||
                    eff.getUsage().equals(EffectUsage.EXHAUST)) &&
                    eff.getSource().isExhausted())
            {
                valid = false;
            }
            if (!valid)
            {
                effList.remove(i);
                i--;
            }
        }
    }

    /**
     * Removes all effects that are not usable because the investigator cannot
     * pay the additional activation cost. Note that this doesn't remove effects
     * for which the investigator would go to 0 sanity or stamina after the
     * cost is paid.
     * @param inv the investigator
     * @param effList the list of effects
     */
    private static void removeUnusableEffects(Investigator inv, List<? extends Effect> effList)
    {
        for (int i = 0; i < effList.size(); i++)
        {
            Effect eff = effList.get(i);
            boolean valid = true;
            if (eff.requiresCast())
            {
                Spell spell = (Spell)eff.getSource();
                if (spell.getSanityCost() > inv.getCurrentSanity())
                {
                    valid = false;
                }
            }
            if (valid && eff.getExtraCosts().size() > 0)
            {
                for (CostType cost : eff.getExtraCosts())
                {
                    if (cost.getType().equals("Movement"))
                    {
                        if (Integer.parseInt(cost.getAmount()) > inv.getMovementPoints())
                        {
                            valid = false;
                            break;
                        }
                    }
                    else if (cost.getType().equals("Stamina"))
                    {
                        if (Integer.parseInt(cost.getAmount()) > inv.getCurrentStamina())
                        {
                            valid = false;
                            break;
                        }
                    }
                    else if (cost.getType().equals("Sanity"))
                    {
                        if (Integer.parseInt(cost.getAmount()) > inv.getCurrentSanity())
                        {
                            valid = false;
                            break;
                        }
                    }
                }
            }
            if (!valid)
            {
                effList.remove(i);
                i--;
            }
        }
    }

    /**
     * Filters a list of EffectTypes, only keeping those that match a particular
     * type.
     * @param list the list to filter
     * @param type the type to match
     */
    public static void filterEffectList(List<EffectType> list, String type)
    {
        for (int i = 0; i < list.size(); i++)
        {
            EffectType eff = list.get(i);
            if (!eff.getType().equals(type))
            {
                list.remove(i);
                i--;
            }
        }
    }

    /**
     * Filters a list of EffectTypes, only keeping those that match a particular
     * details.
     * @param list the list to filter
     * @param type the details to match
     */
    public static void filterEffectsByDetails(List<EffectType> list, String details)
    {
        for (int i = 0; i < list.size(); i++)
        {
            EffectType eff = list.get(i);
            if (!eff.getDetails().equals(details))
            {
                list.remove(i);
                i--;
            }
        }
    }

    /**
     * Filters a list of EffectTypes, only keeping those that match a particular
     * type AND details
     * @param list the list to filter
     * @param type the type to match
     * @param details the details to match
     */
    public static void filterEffectList(List<EffectType> list, String type, String details)
    {
        for (int i = 0; i < list.size(); i++)
        {
            EffectType eff = list.get(i);
            if (!eff.getType().equals(type) || !eff.getDetails().equals(details))
            {
                list.remove(i);
                i--;
            }
        }
    }

    /**
     * Gets the number of hands this effect source uses, if any
     * @param effsrc the effect source to check
     * @return the number of hands that it uses if defined, or 0 if not defined
     */
    public static int getNumHands(EffectSource effsrc)
    {
        if (effsrc instanceof InvestigatorCard)
        {
            InvestigatorCard card = (InvestigatorCard)effsrc;
            if (card.hasProperty("Hands"))
            {
                return XmlTools.parseInt(card.getProperty("Hands"));
            }
            else
            {
                return 0;
            }
        }
        else if (effsrc instanceof PersonalEffect)
        {
            PersonalEffect peff = (PersonalEffect)effsrc;
            if (peff.hasProperty("Hands"))
            {
                return XmlTools.parseInt(peff.getProperty("Hands"));
            }
            else
            {
                return 0;
            }
        }
        else
        {
            return 0;
        }
    }

    /**
     * Shuffles a list
     * @param <E> the type of the list
     * @param list the list to shuffle
     */
    public <E> void shuffleList(List<E> list)
    {
        List<E> newlist = new ArrayList<E>();
        while (!list.isEmpty())
        {
            int index = rng.nextInt(list.size());
            newlist.add(list.remove(index));
        }
        list.addAll(newlist);
    }

    /**
     * Rolls one die and gets the result
     * @return the die roll
     */
    public int rollOneDie()
    {
        return rng.nextInt(6) + 1;
    }

    /**
     * Rolls some dice and gets the result
     * @param numDice
     * @return
     */
    public ArrayList<Integer> rollDice(int numDice)
    {
        ArrayList<Integer> list = new ArrayList<Integer>(numDice);
        for (int i = 0; i < numDice; i++)
        {
            list.add(rollOneDie());
        }
        return list;
    }

}
